React State Management: Zustand vs Redux vs Context Compared
Compare Zustand, Redux Toolkit, and React Context for state management. Learn when each shines, their trade-offs, and how to pick the right tool.
What you'll learn
- ✓When Context is enough and when it is not
- ✓How Zustand achieves minimal boilerplate
- ✓Why Redux Toolkit still wins for large teams
- ✓Performance differences between the three
- ✓How to migrate between them
Prerequisites
- •Comfortable with JS and HTML
- •Basic React component knowledge
What and Why
React ships with useState, useReducer, and Context, which together can model a lot of state. But once your app grows past a single page, sharing state across distant components becomes painful. That is when you start looking at libraries like Redux Toolkit and Zustand.
Choosing badly costs you. Pick Redux for a tiny app and you drown in boilerplate. Pick Context for a huge app and you fight re-renders. Pick Zustand without understanding it and you lose devtools your team loves. This article maps the trade-offs so you can pick with intent.
Mental Model
Think of state management as a spectrum. On one end you have local component state, simple and fast but invisible elsewhere. On the other end you have global stores with strict update rules and time-travel debugging.
- Context is a delivery pipe. It moves a value from a provider to consumers without prop drilling. It does not optimize updates.
- Zustand is a tiny store with hooks. It offers selector-based subscriptions so components only re-render when their slice changes.
- Redux Toolkit is a structured store with reducers, middleware, devtools, and conventions designed for big teams and audited logic.
Context: every consumer re-renders, even if its value did not change
Zustand: only components whose selected slice changed re-render
Redux: only connected components whose selected slice changed re-render Hands-on Example
A Zustand store looks like this:
import { create } from 'zustand';
const useCart = create((set) => ({
items: [],
add: (item) => set((s) => ({ items: [...s.items, item] })),
clear: () => set({ items: [] }),
}));
function Cart() {
const items = useCart((s) => s.items);
return <div>{items.length} items</div>;
}
The selector function (s) => s.items means this component only re-renders when items changes, not when other slices do.
The same logic in Redux Toolkit:
import { createSlice, configureStore } from '@reduxjs/toolkit';
const cart = createSlice({
name: 'cart',
initialState: { items: [] },
reducers: {
add: (s, a) => { s.items.push(a.payload); },
clear: (s) => { s.items = []; },
},
});
const store = configureStore({ reducer: { cart: cart.reducer } });
More files, more naming, but you get structured actions, middleware, and the Redux DevTools that show you every state change.
Plain Context for the same job:
const CartContext = createContext(null);
function CartProvider({ children }) {
const [items, setItems] = useState([]);
return (
<CartContext.Provider value={{ items, setItems }}>
{children}
</CartContext.Provider>
);
}
Simple and fine for small apps, but every consumer of CartContext re-renders when any field on the value changes.
Common Pitfalls
The biggest Context pitfall is treating it like a store. Adding tons of unrelated state to a single context makes every component that reads any part of it re-render on every change. If you find yourself doing that, split contexts or move to a real store.
Zustand pitfalls are usually selector-related. Returning a new object from a selector ((s) => ({ a: s.a })) creates a fresh reference each call, defeating the optimization. Use individual primitive selectors or shallow from Zustand.
Redux’s classic pitfall is over-engineering small features. If a piece of state is local to one page, do not put it in the global store. Redux Toolkit also assumes you understand reducers and Immer, which adds a learning curve for juniors.
A subtler pitfall across all three is mixing server state into your client store. Tools like TanStack Query or RTK Query are designed for server state and handle caching, refetching, and dedupe that you would otherwise build yourself.
Best Practices
- For small apps and component-tree props, stick with
useStateplus Context. - For mid-size apps with cross-cutting state, reach for Zustand. It is tiny and ergonomic.
- For large apps with multiple teams and strict update rules, use Redux Toolkit.
- Keep server state in a query library, not your client state manager.
- Always select the narrowest slice you need, regardless of the library.
Wrap-up
There is no single winner. Context delivers values cheaply but does not optimize. Zustand keeps stores tiny with selector magic. Redux Toolkit gives structure and devtools that scale to teams of many. The right choice is the smallest tool that fits the problem in front of you, not the trendiest. Measure your re-renders, listen to your team, and switch only when you feel real pain.
Related articles
- React React Context vs Redux: When to Use Which
A practical comparison of React Context and Redux: rendering model, performance, devtools, and concrete heuristics for picking the right tool.
- React React Forms: Controlled vs Uncontrolled Inputs Explained
Understand the difference between controlled and uncontrolled inputs in React, when to use each, and how to combine them with refs and form libraries.
- React React State Colocation Patterns: Where State Should Actually Live
A practical guide to deciding where state belongs in a React app, with patterns for lifting, colocating, and splitting state for performance and clarity.
- React React Context API: Sharing State Without Prop Drilling
A practical guide to React's Context API — createContext, Provider, and useContext — with the cases it solves cleanly and the cases where you should reach for a state library instead.