frontend / state managers / jotai_valtio.md

Jotai and Valtio

4 min read source

Jotai and Valtio

Two alternatives to the single-store model (Redux/Zustand), from the Poimandres ecosystem. Jotai is atomic (bottom-up, many small pieces of state that compose). Valtio is proxy-based (mutate a plain object, components re-render on the parts they read).

TL;DR

  • Jotai: state is built from tiny atoms. useAtom(atom) is like useState but the value lives outside the component and is shared. Derived atoms recompute automatically; async atoms integrate with Suspense. Granular by construction — no selector boilerplate to avoid over-rendering.
  • Valtio: you proxy({...}) a mutable object and write to it directly (state.count++). useSnapshot(state) gives an immutable snapshot and tracks exactly which keys a component read, re-rendering only those. Mutable mental model, fine-grained reactivity (very MobX-like).

Interview Q&A

Q: What’s an atom in Jotai?

A: A unit of state with no string key — identity is the atom object:

import { atom, useAtom } from "jotai";

const countAtom = atom(0);
const doubledAtom = atom((get) => get(countAtom) * 2);   // derived, read-only

function Counter() {
  const [count, setCount] = useAtom(countAtom);
  const [doubled] = useAtom(doubledAtom);
  return <button onClick={() => setCount((c) => c + 1)}>{count} / {doubled}</button>;
}

doubledAtom recomputes when countAtom changes; only components using it re-render.

Q: How does Jotai solve the Context re-render problem?

A: React Context re-renders every consumer when the value changes (../05_react/context_performance.md). Jotai splits state into atoms, and a component subscribes only to the atoms it uses — so unrelated updates don’t re-render it. It’s “Context with built-in granularity.” Atoms can be provided per-subtree via <Provider> for scoping/SSR.

Q: What are async atoms?

A: An atom whose read function returns a promise integrates with Suspense:

const userAtom = atom(async (get) => fetch(`/api/user/${get(idAtom)}`).then((r) => r.json()));
// a component reading userAtom suspends until it resolves; refetches when idAtom changes

Useful, but for real server-state caching you still usually want a query library — async atoms are best for derived/coordinated async, not a full cache. See server_vs_client_state.md.

Q: How does Valtio work?

A: Mutate a proxy; read via snapshot:

import { proxy, useSnapshot } from "valtio";

const state = proxy({ count: 0, user: { name: "Ada" } });
function inc() { state.count++; }            // mutate directly, anywhere

function Counter() {
  const snap = useSnapshot(state);           // immutable, render-safe
  return <button onClick={inc}>{snap.count}</button>;  // re-renders only when `count` read changes
}

useSnapshot tracks property access, so a component that reads only count won’t re-render when user.name changes. Mutate state, render from snap.

Q: Atom vs store vs proxy — how do they compare?

A:

Model Library Mental model Granularity
Single store, top-down Redux, Zustand one object, select slices via selectors
Atomic, bottom-up Jotai, Recoil compose many small atoms built-in, per atom
Proxy, mutable Valtio, MobX mutate an object, read snapshots per accessed key

Jotai suits state that’s naturally composed from independent pieces; Valtio suits an OO/mutable mental model; Zustand/Redux suit one cohesive store.

Q: When would you pick these over Zustand/Redux?

A: Jotai when you have lots of small, independent, derivable pieces of state and want zero selector ceremony (e.g., form-field-level state, fine-grained widgets). Valtio when a mutable model is more natural and you want automatic, key-level reactivity without thinking about selectors. Both are small and unopinionated; neither replaces a server-state cache.

Gotchas / edge cases

  • Jotai atom identity matters — define atoms at module scope (stable identity), not inside render, or you create a new atom every render.
  • Jotai derived atoms must be pure in their read function; side effects belong in writable atoms or effects.
  • Valtio: render from the snapshot, mutate the proxy — reading state (not snap) during render skips tracking and causes stale/incorrect renders.
  • Valtio mutations are batched into the next microtask — multiple sync mutations coalesce into one render.
  • SSR: both need per-request scoping (Jotai <Provider> / hydration; Valtio fresh proxy per request) to avoid cross-request state bleed.

What a senior is expected to say

  • “Jotai is bottom-up atoms with built-in granularity — it sidesteps Context’s re-render-everyone problem. Valtio is proxy-based: mutate the object, render from a tracked snapshot.”
  • “Atoms must have stable module-scope identity; Valtio’s rule is mutate the proxy, read the snapshot.”
  • “Both are great for client state; neither replaces a real server-state cache.”

Cross-references

Further reading