Compare trade-offs in state management (Redux, Zustand, Context).
Context: built-in, no extra deps, but every consumer re-renders on any value change — best for low-frequency cross-tree config (theme, auth user). Redux/RTK: predictable + devtools + middleware ecosystem, opinionated boilerplate, best for complex client domains. Zustand: minimal API, selector-based subscriptions, ~1kb, best default for small/medium apps. Server state? None of these — use React Query.
Picking a state manager depends on what kind of state and how often it changes.
Categories of state first
| Kind | Examples | Right tool |
|---|---|---|
| Server state | Lists, single records, mutations | React Query / SWR (NOT Redux/Zustand) |
| Local UI state | input value, modal open | useState |
| Form state | multi-field forms | React Hook Form + Zod |
| Cross-tree config | theme, auth user, feature flags | Context |
| Cross-tree app state | cart, editor doc, design state | Zustand or Redux |
| Routing state | URL params, search | router |
The #1 mistake: putting server state in Redux/Zustand. It's stale by default; you reinvent caching + revalidation badly.
Context
const ThemeCtx = createContext("light");
function App() { return <ThemeCtx.Provider value={theme}><Tree /></ThemeCtx.Provider>; }Strengths: built-in, simplest possible. Weakness: any change to the value re-renders every consumer regardless of which field they read. Fine for theme/auth (changes rarely). Bad for "current selection" in an editor that changes per keystroke.
Redux / RTK
Strengths:
- Single source of truth, time-travel devtools, predictable updates via reducers.
- Middleware ecosystem (logging, thunks, sagas).
useSelectoronly re-renders when the selected slice changes.- RTK collapses boilerplate (
createSlice); RTK Query adds React-Query-like data fetching.
Weaknesses:
- More ceremony than Zustand.
- Heavier bundle (~6kb gz core, more with toolkit).
- Reducer indirection can feel like overkill for simple stores.
When: large complex client domains (Linear-style editor, design tools), teams already familiar, devtools are critical.
Zustand
const useCart = create((set) => ({
items: [],
add: (i) => set((s) => ({ items: [...s.items, i] })),
}));
function Count() { return <span>{useCart((s) => s.items.length)}</span>; }Strengths:
- Tiny (~1kb), no provider needed.
- Selector subscriptions out of the box (only re-renders when selected slice changes).
- Mutate-style API via Immer middleware if desired.
- Easy to test (stores are plain hooks).
Weaknesses:
- Less opinionated structure → teams must agree on patterns (slices, persist, devtools middleware).
- Less mature ecosystem than Redux.
When: most new apps. Sane default.
What about Jotai / Recoil / Valtio?
- Jotai: atoms-as-primary, derived state graph. Great for editor-like apps with many fine-grained pieces.
- Recoil: similar; Facebook-stalled.
- Valtio: proxy-based "mutate freely" model.
Niches; consider when the data graph is genuinely atomic.
Decision tree
Is this data from the server?
→ Yes: React Query / SWR / RTK Query.
→ No:
Cross-tree?
→ No: useState locally.
→ Yes:
Changes rarely (theme, auth)?
→ Context.
Changes often or large state?
→ Zustand (default) or Redux (if devtools/middleware critical).Interview framing
"First I separate server state from client state — server state belongs in React Query, not in Redux/Zustand. For local UI state, useState. For cross-tree config that changes rarely (theme, auth, feature flags), Context. For meaningful client state — cart, editor doc, design tools — Zustand as the default because it's small, has selector subscriptions, and no provider boilerplate. Redux/RTK for large complex domains where time-travel devtools and middleware ecosystem earn their cost. The big anti-pattern is putting server state in a client store and reinventing caching/revalidation."
Follow-up questions
- •Why is Context bad for high-frequency updates?
- •When does Redux's devtools earn its weight?
- •How does Zustand's selector subscription work under the hood?
Common mistakes
- •Server state in Redux/Zustand.
- •Mega-Context that re-renders the whole app on every change.
- •Using Redux for tiny apps with no cross-tree state.
Performance considerations
- •Selector-based subscriptions (Redux useSelector, Zustand selector) prevent fan-out re-renders. Context lacks this and is hot-path-hostile.
Edge cases
- •SSR with Zustand — store created per request to avoid cross-user leak.
- •Context value is a new object every render → tears everything; memoize.
- •Persist middleware (localStorage) + SSR — hydration mismatch.
Real-world examples
- •Zustand: Excalidraw, Linear's web. Redux: legacy Slack, Trello. Jotai: editor-heavy apps. React Query for nearly every server-data app.