RoadmapDay 27 / 80
React (Web)Month 2 · Week 6

Day 27: Atomic & Unidirectional State: Zustand vs Jotai

Understand the two dominant lightweight state models — single-store-with-selectors (Zustand) vs atomic composition (Jotai) — and when to reach for each over Redux.

Mark this day complete

Study

Concepts

Zustand: a minimal single store, subscription-based

Zustand keeps one plain store object (similar shape to Redux) but skips the ceremony: no actions, no reducers, no Provider required — components call a hook (`useStore(selector)`) that subscribes ONLY to the slice returned by the selector function, using reference-equality checks to skip re-renders when that specific slice has not changed. This selector-based subscription model is Zustand's main performance advantage over naively consuming a full Context value (Day 25's problem).

State updates happen by calling `set()` directly from anywhere (no dispatch required), which trades some of Redux's traceability for far less boilerplate — a good fit for small-to-medium apps or isolated feature state that does not need the full action-log/time-travel-debugging machinery.

Jotai: state as a graph of small atoms

Jotai models state as many small, independent "atoms" (`atom(initialValue)`) rather than one big store object. Components subscribe to individual atoms via `useAtom(atomInstance)`, so a component re-renders ONLY when the specific atom(s) it reads change — there is no single store object to accidentally over-subscribe to. Derived atoms (`atom((get) => get(atomA) + get(atomB))`) compose other atoms reactively, similar to a spreadsheet's formula cells, and Jotai tracks these dependencies automatically.

This atomic model shines for state that is naturally per-instance or deeply composable (e.g. many independent form fields, or widgets on a dashboard each with their own settings) where a single shared store would force artificial centralization.

See It

Visualizations

Visualization

Zustand vs Jotai vs Redux Toolkit

 ZustandJotai
Mental modelOne store + selectorsMany small atoms + derived atoms
BoilerplateMinimal — no actions/reducersMinimal — define atoms directly
Re-render granularityPer selected slicePer subscribed atom
Great fit forApp-wide state, feature storesComposable, per-instance state (forms, widgets)
DevTools/time-travelOptional middlewareOptional middleware
vs Redux ToolkitFar less ceremony, less built-in structure/traceabilityFundamentally different mental model, not a drop-in replacement

Build It

Code Examples

A Zustand store with selector-based subscriptions

js
import { create } from 'zustand';

const useCartStore = create((set, get) => ({
  items: [],
  addItem: (item) => set((state) => ({ items: [...state.items, item] })),
  removeItem: (id) =>
    set((state) => ({ items: state.items.filter((i) => i.id !== id) })),
  total: () => get().items.reduce((sum, i) => sum + i.price, 0),
}));

// Subscribes ONLY to 'items' — unrelated store fields changing won't re-render this
function CartBadge() {
  const itemCount = useCartStore((state) => state.items.length);
  return <span>{itemCount}</span>;
}

function AddToCartButton({ product }) {
  const addItem = useCartStore((state) => state.addItem); // stable function ref
  return <button onClick={() => addItem(product)}>Add</button>;
}

Jotai atoms with a derived (computed) atom

js
import { atom, useAtom } from 'jotai';

const cartItemsAtom = atom([]);

// Derived atom: recomputes automatically when cartItemsAtom changes
const cartTotalAtom = atom((get) =>
  get(cartItemsAtom).reduce((sum, item) => sum + item.price, 0)
);

function CartTotal() {
  const [total] = useAtom(cartTotalAtom); // only re-renders when the TOTAL changes
  return <span>${total}</span>;
}

function AddToCartButton({ product }) {
  const [, setItems] = useAtom(cartItemsAtom);
  return (
    <button onClick={() => setItems((prev) => [...prev, product])}>
      Add
    </button>
  );
}

Remember

Key Takeaways

  • Zustand: one store, selector-based subscriptions, minimal API — a lighter-weight Redux alternative.
  • Jotai: many small atoms + derived atoms — a bottom-up, composable model closer to fine-grained reactivity.
  • Both avoid Context's "any change re-renders every consumer" problem by subscribing at a granular level.
  • Neither has Redux's built-in action log/time-travel debugging by default — add middleware if you need that traceability.
  • Pick based on shape of the problem: app-wide store → Zustand/Redux; many independent composable pieces of state → Jotai.

Do It

Practice

  1. 1Rebuild the cart example from Day 26 using Zustand, and confirm CartBadge does not re-render when an unrelated store field changes.
  2. 2Rebuild the same cart using Jotai atoms with a derived cartTotalAtom, and compare the code size/ergonomics to the Redux version.
  3. 3Add zustand/middleware devtools to your store and inspect state changes in Redux DevTools.