Frontend / State managers / 04_jotai_valtio.md

Jotai and Valtio

Updated 3 interview angles 4 min read source
On this page10
  1. What’s an atom in Jotai?
  2. How does Jotai solve the Context re-render problem?
  3. What are async atoms?
  4. How does Valtio work?
  5. Atom vs store vs proxy — how do they compare?
  6. When would you pick these over Zustand/Redux?
  7. Gotchas
  8. Related
  9. Further reading
  10. Interview angle

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).

  • 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).

What’s an atom in Jotai?

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

ts
import { atom, useAtom } from "jotai";

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

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.

How does Jotai solve the Context re-render problem?

React Context re-renders every consumer when the value changes (Context Performance Traps). 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.

What are async atoms?

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

ts
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 State vs Client State.

How does Valtio work?

Mutate a proxy; read via snapshot:

ts
import { proxy, useSnapshot } from "valtio";

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

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.

Atom vs store vs proxy — how do they compare?

Model Libraries How you use it
Single store, top-down Redux, Zustand one object, select slices via selectors
Atomic, bottom-up Jotai, Recoil compose small atoms; per-atom granularity
Proxy, mutable Valtio, MobX mutate, 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.

When would you pick these over Zustand/Redux?

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

  • 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.

Further reading

Interview angle 3

  • “How do Jotai and Valtio differ from Context?” - both give you granular subscriptions, which Context cannot. Jotai is bottom-up: state is composed from small atoms and a component subscribes to the atoms it reads. Valtio is proxy-based: you mutate a plain object and components render from a tracked snapshot, so only the properties actually read cause a re-render.
  • “What is the rule you have to follow in each?” - in Jotai, atoms need stable identity, so they are declared at module scope rather than created inside a component. In Valtio, you mutate the proxy and read the snapshot - reading the proxy directly during render skips the tracking that makes it work.
  • “Do either replace a query library?” - no. They are client-state tools with no notion of staleness, refetching, deduplication or cache invalidation. Putting server responses in them recreates the same problem Redux had.