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 likeuseStatebut 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(notsnap) 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
- Single-store alternative: zustand.md
- Decision matrix across all options: choosing_a_state_library.md
- MobX (the other proxy/observable model): mobx/README.md
- Context re-render problem these solve: ../05_react/context_performance.md
Further reading
- Jotai docs: https://jotai.org/
- Valtio docs: https://valtio.dev/