Chapter 13
Global State — Zustand, Jotai, Recoil, Redux Toolkit
Picking a global-state library for React 19 SPAs — Zustand, Jotai, Recoil, Redux Toolkit. Selectors, render storms, the 7-question checklist, and why useContext is rarely the answer.
Published 2026-05-23
🎯 Chapter Goal — After this chapter you can pick a global-state library based on your team’s needs (not the loudest blog post), write a Zustand store with sliced state and selectors that don’t cause re-render storms, recognise when Jotai’s atomic model is the right fit, know Recoil’s current status (and what to migrate to), and judge when Redux Toolkit still makes sense.
🧭 Prerequisites — Ch 12 (the four kinds of state) — read it first to confirm you actually want global state.
🔹 13.1 Zustand — minimal, store-based
The book’s recommended default. Tiny API, no Provider needed at the root, selectors avoid re-render storms.
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
type CartItem = { id: string; name: string; price: number };
type CartState = {
items: CartItem[];
add: (item: CartItem) => void;
remove: (id: string) => void;
total: () => number;
};
export const useCart = create<CartState>()(
persist(
(set, get) => ({
items: [],
add: (item) => set((s) => ({ items: [...s.items, item] })),
remove: (id) => set((s) => ({ items: s.items.filter((i) => i.id !== id) })),
total: () => get().items.reduce((sum, i) => sum + i.price, 0),
}),
{ name: 'acme-cart' }, // localStorage key
),
);
Consuming with selectors (the avoid-storm pattern):
// Subscribes to the WHOLE store — re-renders on any change. Wrong.
const all = useCart();
// Subscribes to ONE field — re-renders only when items change. Right.
const items = useCart((s) => s.items);
const total = useCart((s) => s.total());
Selectors are the difference between a store that scales and one that doesn’t.
Slicing
const useStore = create<AppState>()((set, get) => ({
...createCartSlice(set, get),
...createAuthSlice(set, get),
...createUiSlice(set, get),
}));
Each slice is a self-contained { state, actions } shape. Compose at the top-level store. Per-slice files keep ownership clear.
Middleware: persist (localStorage / sessionStorage), devtools (Redux DevTools integration), subscribeWithSelector (subscribe outside React), immer (mutate-style updates).
🔹 13.2 Jotai — atomic, derived
The “small atoms” model. Each piece of state is its own atom; components subscribe to specific atoms; derived atoms compose.
import { atom, useAtom } from 'jotai';
const itemsAtom = atom<CartItem[]>([]);
const totalAtom = atom((get) =>
get(itemsAtom).reduce((s, i) => s + i.price, 0),
);
// In a component:
const [items, setItems] = useAtom(itemsAtom);
const total = useAtomValue(totalAtom);
Async atoms for server data (though TanStack Query is usually the better tool — Ch 14):
const userAtom = atom(async () => fetch('/api/me').then((r) => r.json()));
// useAtom suspends until the promise resolves.
When Jotai wins over Zustand:
- Heavy use of derived values that change frequently — Jotai’s dependency tracking is finer-grained.
- You think in atoms rather than stores (a culture/taste call).
- You need each piece of state in its own module without a centralised registry.
When it loses:
- Large monolithic state shapes feel awkward as 200 atoms.
- DevTools experience is weaker than Zustand’s (which piggybacks on Redux DevTools).
🔹 13.3 Recoil — current status and migration
Recoil was Meta’s atomic state library. Status as of 2026: maintenance mode. No active development; bug fixes only.
If you’re already on Recoil: stay until a migration is worth doing; the library still works.
If you’re starting fresh: don’t. The shape of Recoil is closest to Jotai — when you migrate, Jotai is the lowest-friction destination. The mechanical translation:
// Recoil
const itemsState = atom<CartItem[]>({ key: 'items', default: [] });
const totalSelector = selector({
key: 'total',
get: ({ get }) => get(itemsState).reduce(/* … */),
});
// Jotai equivalent
const itemsAtom = atom<CartItem[]>([]);
const totalAtom = atom((get) => get(itemsAtom).reduce(/* … */));
Jotai drops the string keys (Recoil needed them for SSR; Jotai uses references). Otherwise the model is identical.
🔹 13.4 Redux Toolkit / RTK Query — when Redux still makes sense
Redux is not dead. It’s the right tool when:
- You have an existing Redux codebase that works. Migration for migration’s sake is anti-value.
- You need the DevTools experience — time-travel debugging, action replay, structured state inspection. Nothing else has this depth.
- You have a complex state machine with many actions producing many state shapes — Redux’s reducer model excels here.
- You’re using RTK Query as your server-cache solution — it’s a Redux-aware competitor to TanStack Query, fine if you’re already on Redux.
import { createSlice, configureStore } from '@reduxjs/toolkit';
const cartSlice = createSlice({
name: 'cart',
initialState: { items: [] as CartItem[] },
reducers: {
added: (s, a: PayloadAction<CartItem>) => { s.items.push(a.payload); }, // immer-backed
removed: (s, a: PayloadAction<string>) => { s.items = s.items.filter((i) => i.id !== a.payload); },
},
});
export const store = configureStore({ reducer: { cart: cartSlice.reducer } });
Redux Toolkit is the modern Redux API. If you write Redux in 2026, write it this way; the old combineReducers + action-creators ceremony is deprecated style.
🔹 13.5 Picking one — the 7-question checklist
- Does the team already have a global-state library that works? Stay. Migrations are expensive.
- Do you need time-travel debugging? Redux Toolkit.
- Do you think in stores or atoms? Stores → Zustand. Atoms → Jotai.
- Are you mostly fetching server data? Neither — use TanStack Query (Ch 14).
- Do you have heavy derived state that changes frequently? Jotai’s tracking is finer-grained.
- Do you want zero Provider ceremony at the root? Zustand (no provider needed).
- Are you on Recoil today? Stay until a migration buys something concrete; then Jotai.
Recommended default: Zustand. Smallest API, smallest learning curve, no provider ceremony, scales.
🔹 13.6 The “useContext as global state” trap (lab)
The most common bug pattern in 2026 React.
The broken setup:
type AppState = { user: User; theme: Theme; cart: Cart; notifications: Notif[] };
const AppContext = createContext<AppState | null>(null);
// In <App>:
const [state, setState] = useState<AppState>(initial);
<AppContext value={state}>{children}</AppContext>
// In any consumer:
const { theme } = useContext(AppContext); // re-renders on EVERY field change
Profile it (React DevTools Profiler): updating one notification re-renders every component reading theme, every component reading user, every component reading cart.
The fix — Zustand:
const useApp = create<AppState>((set) => ({ /* … */ }));
const theme = useApp((s) => s.theme); // re-renders only on theme change
Or split into multiple contexts (one per concern) if you really want native React. Either way, the lesson is: a single Context for the whole app is an anti-pattern.
🪤 Common Pitfalls
- Reaching for Redux when you really need server cache (TanStack Query).
- Using
useContextfor high-frequency state. - Subscribing to the whole Zustand store instead of selecting.
- Mutating Jotai atoms instead of returning new values.
- Storing form input in a global store.
- Per-component-state Zustand “stores” (
useFooFieldStore) — that’s justuseStatewith extra steps. - Building a Redux Toolkit slice for something three components read once at mount — overkill.
✅ Recap
- Zustand is the safe default in 2026: tiny API, selectors, no provider.
- Jotai if you think in atoms and have derived-heavy state.
- Recoil → migrate to Jotai when a migration pays.
- Redux Toolkit when you have a real Redux codebase or need its DevTools depth.
- Never use a single root Context for unrelated values.
🔗 Further Reading
- https://zustand-demo.pmnd.rs/ — Zustand 5.x docs.
- https://jotai.org/ — Jotai 2.x docs.
- https://redux-toolkit.js.org/ — Redux Toolkit 2.x docs.
- TKDodo’s React Query / state-management series.
- Ch 12 (the four kinds), Ch 14 (TanStack Query — pairs with any of these).
In the book — not on the site
Each topic has an 🧠 Under-the-hood subsection — the algorithm, the data structures, what React DevTools surfaces, debugging recipes. Plus a 🧪 hands-on lab per chapter with a starter repo. Reserved for the book.