Choosing Zustand over Redux for state management
State management has been a hot topic in the React ecosystem for years. While Redux has been the go-to solution for many teams, newer alternatives like Zustand are challenging that dominance. After using both extensively in production applications, I want to share when and why you might choose Zustand over Redux.
The state management landscape
Before diving into the comparison, let's understand what problem we're solving. React's built-in useState and useContext work great for local and moderately shared state. But when you need:
- Global state accessible from anywhere
- Complex state logic
- Time-travel debugging
- Middleware support
- DevTools integration
You need a dedicated state management solution.
What is Redux?
Redux is a predictable state container based on three core principles:
- Single source of truth: All state lives in one store
- State is read-only: Only actions can change state
- Changes via pure functions: Reducers are pure functions
Here's a typical Redux setup:
// Action types
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
// Action creators
const increment = () => ({ type: INCREMENT });
const decrement = () => ({ type: DECREMENT });
// Reducer
const counterReducer = (state = { count: 0 }, action) => {
switch (action.type) {
case INCREMENT:
return { count: state.count + 1 };
case DECREMENT:
return { count: state.count - 1 };
default:
return state;
}
};
// Store
const store = createStore(counterReducer);
// Component usage
import { useSelector, useDispatch } from 'react-redux';
function Counter() {
const count = useSelector(state => state.count);
const dispatch = useDispatch();
return (
<div>
<p>Count: {count}</p>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(decrement())}>-</button>
</div>
);
}
What is Zustand?
Zustand is a small, fast, and scalable state management solution with a much simpler API. It uses hooks and doesn't require providers or boilerplate.
The same counter in Zustand:
import create from 'zustand';
// Create store
const useCounterStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}));
// Component usage
function Counter() {
const { count, increment, decrement } = useCounterStore();
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
Look at that—no providers, no action types, no reducers!
Zustand's bundle size is around 1KB gzipped, while Redux + React-Redux is about 6KB. For simple use cases, that's a significant difference.
Key differences
1. Boilerplate
Redux requires:
- Action types (constants)
- Action creators
- Reducers
- Store configuration
- Provider setup
- Type definitions (for TypeScript)
Zustand requires:
- A store creation function
- That's it!
This difference compounds as your application grows. With Redux, adding a new piece of state means touching multiple files. With Zustand, you just add it to your store.
2. Learning curve
Redux has concepts to learn:
- Actions and action creators
- Reducers and combining reducers
- Middleware
- Thunks or sagas for async
- Selectors and reselect
Zustand concepts:
- Create a store
- Use the store in components
- Optional: middleware
The Zustand learning curve is much gentler, making it great for teams with varying experience levels.
3. TypeScript support
Redux (with Redux Toolkit):
interface CounterState {
value: number;
}
const initialState: CounterState = {
value: 0,
};
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
},
});
export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;
// Type-safe hooks
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
Zustand:
interface CounterStore {
count: number;
increment: () => void;
decrement: () => void;
}
const useCounterStore = create<CounterStore>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}));
Both have good TypeScript support, but Zustand's is more straightforward.
4. Performance optimization
Redux:
- Use
useSelectorwith proper selectors - Memoize selectors with
reselect - Be careful about reference equality
const selectUser = (state) => state.user;
const selectUserName = createSelector(
[selectUser],
(user) => user.name
);
Zustand:
- Built-in shallow equality check
- Select only what you need
// Only re-render when count changes
const count = useCounterStore((state) => state.count);
// Or use shallow for multiple values
import shallow from 'zustand/shallow';
const { count, increment } = useCounterStore(
(state) => ({ count: state.count, increment: state.increment }),
shallow
);
Both can be performant when used correctly, but Zustand's defaults are often good enough.
When to use Redux
Despite Zustand's simplicity, Redux still shines in certain scenarios:
1. Large, complex applications
When you have:
- 50+ components accessing state
- Complex state updates with many side effects
- Need for strict state update patterns
Redux's structure helps maintain organization at scale.
2. Time-travel debugging
Redux DevTools offers powerful debugging capabilities:
- Inspect every action
- Time-travel through state changes
- Replay actions
- Export/import state
This is invaluable for debugging complex state interactions.
3. Established ecosystem
Redux has mature solutions for:
- Persistence (
redux-persist) - Offline support (
redux-offline) - Side effects (
redux-saga,redux-observable) - Form management (
redux-form)
4. Team familiarity
If your team knows Redux well and you have established patterns, the migration cost might not be worth it.
When to use Zustand
Zustand excels when:
1. Starting a new project
Less boilerplate means faster development. You can always migrate to Redux later if needed.
2. Small to medium applications
For apps with:
- Fewer than 20-30 components using global state
- Straightforward state logic
- Limited async operations
Zustand provides everything you need without the overhead.
3. Incremental adoption
Zustand doesn't require providers, so you can:
- Add it to existing apps easily
- Use it alongside other state solutions
- Migrate piece by piece
4. Bundle size matters
For apps where every kilobyte counts (mobile web, embedded), Zustand's tiny footprint is a win.
Real-world example: Shopping cart
Let's compare a shopping cart implementation:
Zustand:
import create from 'zustand';
import { persist } from 'zustand/middleware';
const useCartStore = create(
persist(
(set, get) => ({
items: [],
addItem: (product) => set((state) => ({
items: [...state.items, { ...product, quantity: 1 }]
})),
removeItem: (id) => set((state) => ({
items: state.items.filter(item => item.id !== id)
})),
updateQuantity: (id, quantity) => set((state) => ({
items: state.items.map(item =>
item.id === id ? { ...item, quantity } : item
)
})),
clearCart: () => set({ items: [] }),
total: () => {
const { items } = get();
return items.reduce((sum, item) =>
sum + item.price * item.quantity, 0
);
}
}),
{ name: 'shopping-cart' }
)
);
Usage in component:
function Cart() {
const items = useCartStore((state) => state.items);
const removeItem = useCartStore((state) => state.removeItem);
const total = useCartStore((state) => state.total);
return (
<div>
{items.map(item => (
<div key={item.id}>
{item.name} - ${item.price} x {item.quantity}
<button onClick={() => removeItem(item.id)}>Remove</button>
</div>
))}
<p>Total: ${total()}</p>
</div>
);
}
Clean, simple, and includes persistence!
Zustand's middleware system is powerful yet simple. You can add persistence, dev tools, and custom middleware without much overhead.
Migration tips
If you're considering migrating from Redux to Zustand:
- Start with new features - Don't rewrite everything at once
- Identify simple slices first - Migrate straightforward state first
- Coexist peacefully - Both can live in the same app
- Measure impact - Track bundle size and performance improvements
- Update incrementally - No rush to migrate everything
My recommendation
After using both in production:
Use Zustand if:
- You're starting fresh
- You value developer experience
- Bundle size matters
- Your state logic is straightforward
- You want less boilerplate
Use Redux if:
- You have a large, complex app
- Time-travel debugging is essential
- You need the mature ecosystem
- Your team is already proficient with it
- You have complex middleware requirements
Use Redux Toolkit if you must use Redux - it reduces much of the boilerplate that makes Redux painful.
Conclusion
Zustand represents a new generation of state management—simpler, smaller, and more aligned with how we write React today. It's not trying to replace Redux in all scenarios, but for many applications, it's a better fit.
The JavaScript ecosystem is moving toward simplicity and better defaults. Zustand embodies this philosophy. Unless you have specific needs that Redux addresses, I'd recommend starting with Zustand and only reaching for Redux when you truly need its power.
Remember: the best state management solution is the one that helps your team ship features faster with fewer bugs. Choose based on your actual needs, not hype or tradition.
Happy state managing! 🎯