Zustand vs Redux

State management in React has evolved from the early days of Prop Drilling to the structured world of Redux, and now to the minimalist era of Hooks-based libraries like Zustand. Understanding the trade-offs between these two giants is crucial for any React developer.

1. The Core Philosophy

Redux: The Centralized Bureaucracy

Redux is built on the principle of a Single Source of Truth. The entire application state is stored in a single object tree within a single store. To change the state, you must dispatch an action, which describes what happened, and write a reducer, which describes how the state transforms.

  • Pros: Predictable, easy to debug (Redux DevTools), strict structure (good for large teams), middleware ecosystem.
  • Cons: Verbose boilerplate, steep learning curve, “files jumping” (action → type → reducer → selector).

Zustand: The Agile Team

Zustand (German for “State”) takes a minimalist approach. It uses hooks to create a store. You don’t need to wrap your app in a provider. It’s unopinionated and lets you structure your state however you like.

  • Pros: Almost zero boilerplate, simple API, no provider wrapper, flexible (can be centralized or atomic).
  • Cons: Less structure enforced (can lead to messy code in large teams if not careful), fewer built-in patterns than Redux Toolkit.

2. Interactive: Boilerplate Visualizer

Drag the required pieces to build a simple “Counter” feature in both libraries. Notice how many pieces you need for Redux versus Zustand.

Redux Architecture

Drop Redux parts here
0/5 Assembled

Zustand Store

Drop Zustand parts here
0/2 Assembled

Drag blocks to build the 'Counter' feature:

Action Type
Action Creator
Reducer
Store Provider
Selector
Create Hook
Use Store

3. Code Comparison

Let’s implement a simple counter with increment, decrement, and reset functionality.

The Redux Way (with Redux Toolkit)

Even with Redux Toolkit (which simplifies things), there’s still a specific structure to follow.

// 1. Slice (Actions + Reducers)
import { createSlice, configureStore, PayloadAction } from '@reduxjs/toolkit';

// Define the State Interface
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 },
    reset: (state) => { state.value = 0 },
    // Payload example
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload
    },
  },
});

export const { increment, decrement, reset } = counterSlice.actions;

// 2. Store Configuration
export const store = configureStore({
  reducer: {
    counter: counterSlice.reducer,
  },
});

// Types for Dispatch and State
type RootState = ReturnType<typeof store.getState>;
type AppDispatch = typeof store.dispatch;

// 3. Usage in Component
import { useSelector, useDispatch, TypedUseSelectorHook } from 'react-redux';

// Use typed hooks throughout the app
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

export function Counter() {
  const count = useAppSelector((state) => state.counter.value);
  const dispatch = useAppDispatch();

  return (
    <div>
      <span>{count}</span>
      <button onClick={() => dispatch(increment())}>+</button>
      <button onClick={() => dispatch(decrement())}>-</button>
    </div>
  );
}

The Zustand Way

Zustand collapses the store creation, actions, and reducers into a single hook.

import { create } from 'zustand';

// 1. Create Store Hook
interface CounterState {
  count: number;
  increment: () => void;
  decrement: () => void;
  reset: () => void;
}

const useStore = create<CounterState>((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
}));

// 2. Usage in Component
export function Counter() {
  // Direct access to state and actions
  const { count, increment, decrement } = useStore();

  return (
    <div>
      <span>{count}</span>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  );
}

4. Architectural Diagram: Flux vs Atomic

Redux (Flux) Zustand (Atomic) View Action Store View Store (State + Actions)

5. When to Use Which?

Feature Redux (Toolkit) Zustand
Boilerplate High (Slice, Store, Provider) Low (Just a hook)
Structure Strict (Actions separate from State) Flexible (Actions in State)
DevTools Excellent (Time travel built-in) Good (via Middleware)
Bundle Size Larger Tiny (<1kB)
Best For Large teams, complex state, legacy apps Startups, side projects, modern apps

[!TIP] Start with Zustand. If you find yourself needing more rigid structure or complex middleware pipelines (like Sagas/Observables), you can migrate to Redux. But 95% of apps today don’t need the complexity of Redux.