React Context vs Zustand: When Each One Actually Makes Sense

"Context causes re-renders, use Zustand instead" is the most repeated — and most misunderstood — piece of React state management advice. Here's what's actually true, and the decision rule that makes the choice obvious.
Every few months someone posts a benchmark showing React Context causing excessive re-renders and concludes you should replace it with Zustand everywhere. The post gets thousands of shares. Developers swap out their context providers. The problem they were solving may or may not have actually existed.
Let me give you a more useful frame.
The myth first
Context does not inherently cause re-render problems.
This is worth stating clearly because the opposite is repeated so often it feels like established fact. The truth is more specific: a context provider that holds frequently-changing values will cause re-renders in every consumer when those values change.
That's a meaningful distinction. The problem isn't context. It's putting the wrong kind of state into context.
Here's the pattern that causes problems:
The issue here is that the value object is recreated on every render of CartProvider. Every consumer sees a new reference and re-renders. This is fixable — useMemo on the value, splitting into separate contexts for state and dispatch — but it takes deliberate effort.
Zustand handles this without the effort. Which is why it feels better for this use case. But that doesn't mean context is wrong everywhere.
What context is actually good for
Context was designed for values that are:
- Read by many components across different tree depths
- Updated infrequently — theme, locale, authenticated user, feature flags
- Shared configuration that components need to access without prop drilling
Here's context used correctly:
Theme changes happen rarely — a user toggles it once per session at most. Every consumer re-rendering when theme changes is fine because it should re-render — its appearance depends on the theme.
The same logic applies to:
For all of these, context is the right tool. Zustand would work too, but it would be more code for no additional benefit.
What Zustand is actually good for
Zustand earns its place when state is:
- Updated frequently — shopping cart, form state, real-time data
- Read by many components with different slices — one component needs the cart count, another needs the full item list
- Complex enough to benefit from actions — derived state, async updates, middleware
Here's the same shopping cart in Zustand:
Now different components subscribe to only the slice they need:
Each component subscribes to a selector. If a different part of the store changes, that component doesn't re-render. This is the Zustand feature that context genuinely can't match without significant extra work.
The side-by-side comparison
Here's the same feature built both ways — a user preferences object that includes notification settings, display density, and sidebar state.
With context:
With Zustand:
The Zustand version is less code. More importantly, a component that only needs sidebarOpen won't re-render when density changes — context can't do that without splitting into multiple providers.
For preferences that update occasionally, the difference is negligible. For preferences that update on every keystroke or scroll event, Zustand wins clearly.
The decision flowchart
Ask these questions in order:
1. How often does this state change?
- Rarely (theme, locale, auth user) → context is fine
- Frequently (cart, form, real-time data) → Zustand
2. Do different components need different slices of the same state?
- No, they all need the same thing → context is fine
- Yes, each needs a different subset → Zustand
3. Does the state have complex update logic?
- Simple
setStatecalls → context is fine - Derived values, async actions, middleware → Zustand
If you answered "context is fine" to all three, use context. If you answered Zustand to any one of them, Zustand is the better fit.
The one thing context does that Zustand doesn't
Context is scoped to a React subtree. A ThemeProvider wrapping one part of the app provides a different theme to that subtree than the ThemeProvider wrapping the rest. Zustand is global — one store, one value, everywhere.
This matters for component libraries, for testing (you can wrap a component in a test-specific provider with different values), and for features like themes that need to be overridable at different tree depths.
If you need scoped state, context is the right tool regardless of update frequency. Zustand global state can't replicate this behavior.
What to do with an existing codebase full of context
Don't rewrite it. Context that's working fine — low-frequency updates, no performance complaints from users — doesn't need to be replaced.
The time to reach for Zustand is when:
- You're profiling a real performance problem and the flame graph shows context-triggered re-renders as the bottleneck
- You're designing new state that will update frequently or need selective subscriptions from the start
Premature optimization is still optimization. The benchmark someone posted on Twitter is not your application's production profile.
Context and Zustand solve overlapping but not identical problems. The developers who use context everywhere hit re-render issues and blame context. The developers who use Zustand everywhere write more code than they need to for global config values. The right answer is using each for what it was designed for.
Up next: Stop colocating everything — a better way to structure React features — the folder structure that scales without becoming a maze.
Related: How to manage global state without a library — the useSyncExternalStore pattern for when you don't want either.
Alex Chen is a senior frontend engineer who writes about React patterns, JavaScript internals, and the decisions that separate maintainable codebases from ones that fight back. Opinionated by design.
Photo by Henry Schneider on Unsplash

