Context Api vs Zustand
React's built-in Context API versus Zustand for client state. One ships with React and re-renders the world; the other is 1KB and surgical. We pick a winner.
The short answer
Zustand over Context Api for most cases. Context broadcasts every change to every consumer with no selective subscription, so it rots into re-render soup the moment your app grows.
- Pick Context Api if your state is tiny, near-static, and read by a handful of components — theme, locale, current user, an auth token. Context is built in, zero dependencies, and perfectly fine for low-frequency values
- Pick Zustand if have state that changes often, is read by many components, or you're sick of memoizing context values and splitting providers to dodge re-renders. This is most apps
- Also consider: Server state? Neither. Reach for TanStack Query or RTK Query — Context and Zustand are both client-state tools, and stuffing fetched data into either is a category error people make constantly.
— Nice Pick, opinionated tool recommendations
The core difference
Context API is a dependency-injection mechanism that React bolts on for free. It moves a value down the tree without prop drilling — that's its entire job. It is NOT a state manager, no matter how many tutorials pretend otherwise. Zustand is a purpose-built store: you create state outside React, components subscribe to slices of it via selectors, and only the components reading a changed slice re-render. The architectural gap is selective subscription. Context has none — every consumer re-renders when the provider value changes by reference, full stop. Zustand was designed around exactly this problem. So the comparison isn't 'simple vs complex,' it's 'a wire vs a switchboard.' People reach for Context because it's already there, then spend weeks engineering around its one fatal limitation. That's the whole story, and it decides almost everything below.
Performance and re-renders
This is where Context earns its reputation. A Context value is a single object; change one field and React re-renders every consumer of that provider, regardless of which field they read. The standard 'fixes' are ugly: split into multiple providers, wrap values in useMemo, memoize every consumer with React.memo, or hand-roll a useContextSelector. You are now reimplementing Zustand badly. Zustand sidesteps all of it: const count = useStore(s => s.count) subscribes to that slice only, and a change to s.user won't touch this component. It uses strict-equality (or your custom comparator) to decide. For a settings panel that updates twice a session, Context's broadcast costs nothing. For a cart, a canvas, a live dashboard, or anything updating per-keystroke, Context will tank your frame budget and you'll feel it. Zustand simply doesn't have this failure mode.
Developer experience
Context's ergonomics are deceptively rough. You write a context, a provider, a custom hook, often a reducer, and you nest providers until your tree looks like a Russian doll. Updating state from deep children means threading dispatch through — more boilerplate, the exact thing Context was supposed to kill. Zustand is almost rude in its simplicity: one create call returns a hook, you read with selectors, you write by calling actions defined in the store, and there's no provider to mount at all. State lives in a module, so you can read or mutate it outside React too — handy in event handlers, web socket callbacks, or tests. Middleware for persistence, immer, and devtools is a one-line wrapper. Context gives you none of that; you build it. The only DX point Context wins is 'nothing to install,' which matters for about a day.
When Context actually wins
I'm not pretending Context is useless — it wins a real, narrow band. If a value is set once and rarely changes — theme, locale, feature flags, the authenticated user, a DI handle to some service — Context is the correct, idiomatic tool and pulling in Zustand would be over-engineering. Library authors especially should prefer Context: shipping a store dependency onto consumers is presumptuous, while Context is zero-footprint and composable. Context also pairs naturally with useReducer for genuinely local, scoped state that shouldn't be global at all — Zustand's module-level store is the wrong shape for 'this state belongs to this subtree.' So the honest rule: Context for low-frequency, injected, or scoped values; Zustand for shared, mutating application state. Most teams misclassify their state as the former when it's screamingly the latter, then blame React for being slow.
Quick Comparison
| Factor | Context Api | Zustand |
|---|---|---|
| Selective subscriptions | None — all consumers re-render on any value change | Per-slice selectors with custom equality |
| Bundle cost | 0 KB (built into React) | ~1 KB gzipped |
| Boilerplate | Provider + custom hook + often a reducer; nested provider trees | One create() call, no provider needed |
| Use outside React | No — coupled to the component tree | Yes — read/write the store anywhere |
| Best fit | Low-frequency injected/scoped values (theme, user, DI) | Shared, frequently-changing app state |
The Verdict
Use Context Api if: Your state is tiny, near-static, and read by a handful of components — theme, locale, current user, an auth token. Context is built in, zero dependencies, and perfectly fine for low-frequency values.
Use Zustand if: You have state that changes often, is read by many components, or you're sick of memoizing context values and splitting providers to dodge re-renders. This is most apps.
Consider: Server state? Neither. Reach for TanStack Query or RTK Query — Context and Zustand are both client-state tools, and stuffing fetched data into either is a category error people make constantly.
Context broadcasts every change to every consumer with no selective subscription, so it rots into re-render soup the moment your app grows. Zustand gives you selector-based subscriptions, no provider tree, and a smaller bundle. It's the right default for any real state.
Related Comparisons
Disagree? nice@nicepick.dev