Skip to content
State Management & Store Libraries

Building UIs

Listen 0%
Speed

06 · State Management & Store Libraries

The hardest part of frontend is not rendering — it’s deciding where state lives and who owns it. This file covers the taxonomy of state, the mechanics that distinguish store libraries, and a head-to-head of Redux Toolkit, Zustand, Jotai, Valtio, MobX, XState, and Signals. Reflects the 2026 landscape (Zustand-led client state, TanStack Query owning server state, Recoil archived).


Positioning

Most “state management” debates are really category errors — people use one tool for kinds of state that want different tools. Get the taxonomy right and library choice becomes easy.


Foundations: the five kinds of state

  1. Server state — data that lives on a server and is cached on the client (API responses). It’s asynchronous, shared, can go stale, and needs caching, revalidation, deduplication, retries. This is the majority of real app state, and it should NOT live in Redux/Zustand. Use a server-state library: TanStack Query (React Query), SWR, RTK Query, Apollo (GraphQL), or RSC + Server Actions (05/08).
  2. Global client (UI) state — genuinely app-wide, client-only: theme, auth status, feature flags, a cart, modal/toast registry. This is what client store libraries are for, and it’s smaller than people think.
  3. Local component stateuseState/useReducer. Colocate; lift only when shared.
  4. URL state — filters, tabs, pagination, selected IDs — anything shareable/bookmarkable belongs in the URL (useSearchParams/router). Underused; it’s free persistence and shareability.
  5. Form state — high-frequency, validation-heavy. Use React Hook Form (+ Zod) or framework form actions; don’t push every keystroke into global state.

The senior heuristic: server state → TanStack Query; URL state → router; form state → RHF; local → useState; only what’s left → a global store. Following this dissolves most “we need Redux” conversations.


Deep dive

1. The mechanics that differentiate stores

Every store library is some answer to four questions:

  • How is state held? Single immutable tree (Redux), multiple mutable-ish stores (Zustand), atoms (Jotai/Recoil), a proxy you mutate (Valtio/MobX), or a state machine (XState).
  • How do updates happen? Dispatched actions + reducers (Redux), setter functions (Zustand), atom setters (Jotai), direct mutation intercepted by a Proxy (Valtio/MobX), or events/transitions (XState).
  • How do components subscribe & re-render? Selector subscriptions (Redux useSelector, Zustand selectors), per-atom subscriptions (Jotai — only consumers of a changed atom re-render), proxy access tracking (Valtio/MobX — components re-render based on exactly which fields they read), or machine snapshots (XState).
  • What’s the granularity? Coarse (whole-store selectors) vs fine-grained (atomic/signal — only the exact reader updates). Granularity drives re-render performance.

Two cross-cutting concepts:

  • Tearing — under React’s concurrent rendering, an external store can be read at two different values within one render pass, producing inconsistent UI. The fix is useSyncExternalStore (React 18+), which all modern stores use internally. This is the reason hand-rolled useState+context stores are risky under concurrency.
  • Immutability — Redux requires it (enables time-travel, cheap equality checks, predictability); Immer (bundled in Redux Toolkit) lets you write mutable-looking code that produces immutable updates via a Proxy. Valtio/MobX invert this: you mutate, they track.

2. The library head-to-head

Redux Toolkit (RTK) — the structured heavyweight

  • Model: single immutable store, createSlice (reducers via Immer), typed actions, middleware, RTK Query for server state, Redux DevTools time-travel.
  • Strengths: predictability, debuggability, enforced patterns, huge ecosystem, great for large teams and complex cross-cutting state. Time-travel/replay is genuinely unique.
  • Costs: ~15 KB with react-redux; more ceremony; easy to overuse for things that are really server state.
  • Use when: 10+ devs, complex shared state, you need enforced architecture and serious devtools. RTK Query can cover server state if you’re already invested.

Zustand — the pragmatic default

  • Model: a tiny (~3 KB) hook-based store; create((set, get) => ({...})); mutate via set; subscribe with selectors. Works outside React too (call store.getState() anywhere). Middleware for persistence, immer, devtools.
  • Strengths: minimal boilerplate, excellent performance with selectors, simple mental model, no provider required.
  • Costs: less prescriptive (a feature and a risk); no built-in server-state caching (pair with TanStack Query).
  • Use when: most small-to-medium apps and a lot of large ones. The 2026 community default for global client state.

Jotai — atomic, bottom-up

  • Model: primitive atom()s composed into derived atoms; useAtom subscribes a component to only that atom. Inspired by Recoil but smaller and maintained (Recoil is archived).
  • Strengths: fine-grained reactivity (only readers of a changed atom re-render), great for lots of independent/derived state, no central store, minimal re-renders.
  • Costs: atom sprawl in large apps; a different mental model than “a store.”
  • Use when: dashboards, editors, forms with many interdependent derived values; when Context causes re-render storms.

Valtio — proxy-based, mutable ergonomics

  • Model: proxy({...}); mutate directly (state.count++); useSnapshot gives an immutable snapshot and tracks exactly which fields a component reads, re-rendering only those.
  • Strengths: the most natural mutation API; precise render tracking; from the Zustand/Jotai authors (Poimandres).
  • Costs: Proxy “magic” can surprise; debugging mutation sources is less explicit than dispatched actions.
  • Use when: you want mutable ergonomics with fine-grained subscriptions and don’t need action-log auditability.

MobX — battle-tested reactive OOP

  • Model: observable state + computed values + reactions; transparent reactive programming via Proxies. makeAutoObservable.
  • Strengths: powerful derived/computed graph, mature, scales to complex domains, great with class-based domain models.
  • Costs: larger, a distinct paradigm, less “Reacty” in 2026; smaller momentum than the Poimandres trio.
  • Use when: complex domain models with lots of derived state, or teams comfortable with reactive OOP.

XState — explicit state machines & statecharts

  • Model: finite state machines / statecharts: explicit states, events, transitions, guards, actors. Not a general store — a way to model complex flows (multi-step wizards, checkout, async orchestration, retries).
  • Strengths: makes illegal states impossible (mirrors discriminated unions, 03), visualizable, eliminates boolean-flag soup (isLoading && !isError && hasData), great for complex UI logic.
  • Costs: overkill for simple state; learning curve.
  • Use when: genuinely complex, multi-step, or async-heavy flows where correctness matters.

Signals (@preact/signals, and the TC39 proposal)

  • Model: fine-grained reactive primitives; reading a signal in a component subscribes it; updates can bypass React’s reconciliation and patch the DOM directly in some integrations.
  • Strengths: the best raw update performance (near-direct DOM updates), tiny, the model underpinning SolidJS/Vue/Svelte 5 runes.
  • Costs: React integration is still maturing (the TC39 Signals proposal is in committee); a different reactivity model than idiomatic React.
  • Use when: perf-critical fine-grained updates; watch the standardization track.

Nanostores — tiny, framework-agnostic

  • Model: atomic stores (<1 KB), framework-agnostic (React/Vue/Svelte/vanilla). Fast-growing.
  • Use when: shared state across micro frontends or multiple frameworks, or when bundle size is paramount.

3. Server state deserves its own tool

TanStack Query (React Query) is the near-universal 2026 recommendation for server state. It gives you: caching keyed by query, deduplication, background refetch, stale-while-revalidate, retries, pagination/infinite, optimistic updates, and request cancellation (AbortSignal). It removes ~80% of what people used to (mis)put in Redux. SWR is the lighter Vercel alternative; RTK Query integrates if you’re in Redux; Apollo/urql/Relay for GraphQL; RSC + Server Actions push server state to the server entirely (08).

The most important state-management decision in 2026 is “server state goes in TanStack Query, not in my client store.” Everything else is secondary.

4. Decision framework

Is it server data (came from / goes to an API)?  ── yes ─▶ TanStack Query / RTK Query / RSC
Is it shareable / bookmarkable (filters, tabs)?  ── yes ─▶ URL (router searchParams)
Is it form input?                                ── yes ─▶ React Hook Form (+ Zod)
Is it used by one subtree only?                  ── yes ─▶ useState / useReducer (colocate)
Is it a complex multi-step / async flow?         ── yes ─▶ XState
Genuinely global client UI state?                ── yes ─▶ Zustand (default) · Jotai (atomic) · RTK (enterprise)

Worked example: the same counter, four ways

// Zustand
const useStore = create((set) => ({ count: 0, inc: () => set(s => ({ count: s.count + 1 })) }));
const count = useStore(s => s.count);        // selector → minimal re-render

// Jotai
const countAtom = atom(0);
const [count, setCount] = useAtom(countAtom); // only atom readers re-render

// Valtio
const state = proxy({ count: 0 });
const snap = useSnapshot(state);              // state.count++ to update; reads tracked

// Redux Toolkit
const slice = createSlice({ name: 'c', initialState: { count: 0 },
  reducers: { inc: (s) => { s.count++; } }}); // Immer makes this immutable under the hood
const count2 = useSelector(s => s.c.count);

Same outcome; the difference is boilerplate, subscription granularity, and what you can audit.


Pitfalls & gotchas

  • Putting server state in a client store — the original sin; causes stale data, manual cache code, and bugs. Use TanStack Query.
  • Context as a state manager — every consumer re-renders on any change; split contexts or use a store with selectors.
  • One giant global store for everything — couples unrelated features; colocate instead.
  • Tearing from hand-rolled stores under concurrent rendering — use useSyncExternalStore (or a library that does).
  • Selectors returning new object references each call — defeats memoized equality and re-renders constantly; return primitives or use shallow/useShallow.
  • Over-reaching for XState/Redux when colocated useState would do.
  • Storing derived data instead of deriving it — keep one source of truth; compute the rest.

Interview / self-test questions

  1. Name the five kinds of state and the right tool for each.
  2. Why shouldn’t server state live in Redux/Zustand? (Caching/staleness/dedup/retries are a different problem; TanStack Query solves them.)
  3. What is tearing and how is it prevented? (Inconsistent reads under concurrency; useSyncExternalStore.)
  4. Zustand vs Jotai vs Redux Toolkit — model and ideal use of each.
  5. How does Immer let you “mutate” while staying immutable? (Proxy that records changes and produces a new tree.)
  6. When is XState the right call over a reducer/store?
  7. Why does Context cause re-render storms, and what are the fixes?
  8. What does atomic/fine-grained reactivity buy you over a single-store selector model?
  9. Why is Recoil no longer recommended? (Archived/unmaintained; Jotai is the spiritual successor.)

Recommendations

  • Default stack: TanStack Query (server) + Zustand (global client) + React Hook Form + Zod (forms) + URL for shareable state + useState for local.
  • Reach for Jotai when Context causes re-render problems and your state is atomic/derived.
  • Reach for Redux Toolkit on large teams needing enforced patterns and time-travel.
  • Reach for XState for complex flows; for everything else, prefer the simplest tool.
  • Always use selectors (Zustand/Redux) or atoms (Jotai) to scope re-renders; return stable references.
  • Keep one source of truth; derive everything else.

Books & references

  • TanStack Query docs (tanstack.com/query) — and the broader argument that server state is a distinct concern; read this before choosing a client store.
  • Redux docs / “Redux Essentials” (redux.js.org) — even if you won’t use it, the Flux/unidirectional-flow concepts are foundational.
  • Zustand, Jotai, Valtio docs (Poimandres / pmnd.rs) — small, readable, opinionated.
  • XState docs & “Statecharts” (statecharts.dev) — David Harel’s statechart formalism is worth knowing.
  • “State of React” survey (published early each year) — the empirical adoption/satisfaction signal.
  • Kent C. Dodds, “Application State Management with React” and “Don’t Sync State, Derive It” — the colocation/derivation philosophy.

Connections

Frontend Deep-Dive Library · content is the single source of truth.