Context vs Redux vs Zustand — when to use what?
Context for low-frequency cross-cutting values (theme, locale). Zustand for medium app-wide state with selector-based subscriptions. Redux Toolkit when you need devtools, time-travel, and a strict update protocol.
The single biggest mistake when comparing these three is treating them as alternatives to the same job. They aren't. Context, Zustand, and Redux solve overlapping but different problems, and the right modern app often uses all three plus a server-cache library. Picking correctly comes down to: (a) is this server state or client state? (b) how often does it change? (c) do many components subscribe to different slices of it?
Context is a value distribution primitive, not a state manager. React.createContext lets a parent broadcast a value down the tree; descendants opt in with useContext. Crucially, there is no selector layer: when the provider's value reference changes, every consumer re-renders, full stop. That's fine for values that change rarely (theme, locale, auth user, feature-flag map, DI container references) and ruinous for anything updated at interactive frequencies. Context combined with useReducer is sometimes called "the poor man's Redux," and that's exactly when people learn the no-selector problem — toggling one item in a 200-item list re-renders the whole tree.
Zustand is a tiny (~1KB gzipped) store outside React. create() returns a hook; consumers pass a selector useStore(s => s.something) and only re-render when that slice changes by reference. No provider needed; the store is just a module export. Pros: trivial to learn, no boilerplate, selectors give per-slice subscriptions, integrates with useSyncExternalStore for tear-free concurrent reads, supports middleware (persist, devtools, immer). It's an excellent default for medium-complexity client state — cart, multi-step wizard, modal stack, UI prefs.
Redux Toolkit is the heavyweight. Strict action/reducer protocol, RTK Query for server cache, time-travel devtools, middleware (thunks, sagas, listeners), structured serialization, opinionated slice pattern. The cost is verbosity and ceremony. It pays off when (a) the team is large enough that the strict protocol prevents architectural drift, (b) you need rich devtools for debugging complex flows (financial dashboards, animation editors), (c) you need undo/redo or time travel, (d) you have a structured server-cache problem that benefits from RTK Query's tagged invalidation. For a 5-person team building a CRUD app, Redux is overkill.
Server state is a separate category. It is not a "client state manager" problem. TanStack Query, SWR, RTK Query, Apollo, urql all specialize here: caching, deduplication, request coalescing, background refetch, mutation invalidation, optimistic updates, polling. Use one of them for anything that originated on the server. Don't dump it into Zustand or Redux and reinvent the cache.
Decision matrix:
| Need | Choose |
|---|---|
| Theme, locale, auth user object | Context |
| Toast queue, modal stack | Zustand |
| Cart, filters, wizard, UI prefs | Zustand |
| Server data, mutations, cache | TanStack Query / RTK Query |
| Time travel, undo/redo, devtools | Redux Toolkit |
| Form state | React Hook Form |
| Atomic, dependency-graph derived state | Jotai / Recoil |
| Cross-tab sync, persistence | Zustand + persist middleware |
Modern stack pattern I see most often in 2025: Server state in TanStack Query (or RSC + Server Actions for Next.js), client state in Zustand (one feature-scoped store per feature), cross-cutting values in Context (theme, auth user). Redux Toolkit only when its specific affordances earn rent. Jotai / Recoil when the state is naturally a graph of small derived atoms (Figma-style apps).
Common pitfalls:
- "Just use Context" for everything → re-render storms. Split contexts by update frequency.
- Redux for a small app → ceremony with no payoff.
- Putting server data in Redux without an RTK Query layer → reinventing cache invalidation badly.
- Zustand stores at module scope persisting across SSR requests → memory leaks or cross-user data leaks. Use a per-request store with React's
use()or pass via Context.
Pick by change frequency × number of consumers × need for time travel, not by Twitter trend.
Code
Follow-up questions
- •Why does Context cause perf issues in a fast-updating store?
- •How does Zustand achieve no-provider stores?
- •When does TanStack Query overlap with these?
Common mistakes
- •Using Context for high-frequency state (mouse position, scroll) — re-renders cascade.
- •Storing server data in Redux when TanStack Query handles caching/invalidation for free.
Performance considerations
- •Context value identity matters — wrap providers with `useMemo` or split into multiple contexts.
- •Zustand selectors should return primitives or use shallow equality.
Edge cases
- •Context + `useReducer` looks like a Redux replacement until your app grows; reach for a real store before re-render storms hit.
Real-world examples
- •shadcn/ui ships theme via Context; product surfaces use Zustand for filters; payment flows use Redux Toolkit for the strict step machine.