Skip to main content

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:

  1. Single source of truth: All state lives in one store
  2. State is read-only: Only actions can change state
  3. 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!

info

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 useSelector with 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!

tip

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:

  1. Start with new features - Don't rewrite everything at once
  2. Identify simple slices first - Migrate straightforward state first
  3. Coexist peacefully - Both can live in the same app
  4. Measure impact - Track bundle size and performance improvements
  5. 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! 🎯