ReactTypeScriptState Management

Zustand vs Redux: Choosing the Right State Management Approach in React

A deep comparison of Zustand and Redux Toolkit — what they solve, how they differ in mental model, and when to reach for each one.

14 min read

There is a point in almost every React developer's journey where useState and useContext stop feeling sufficient.

Maybe you have authentication state that several parts of your application need to access. Maybe a shopping cart needs to be shared between completely different components. Maybe you have filters, selected items, modals, user preferences, or application-wide settings that multiple parts of your application need to read and update.

That's usually when the question appears:

"Should I use Redux?"

And for a long time, Redux was almost synonymous with global state management in the React ecosystem. It is powerful, predictable, mature, and has an enormous ecosystem.

But there is another question worth asking:

"Do I actually need all of Redux's concepts for the problem I'm solving?"

This is where Zustand becomes interesting.

First: Why Do We Need State Management?

Before comparing libraries, let's understand the problem they're solving. Imagine a small application where the navbar needs the user, the dashboard needs the user, the products page needs the cart, and the checkout page also needs the cart.

You could pass everything through props — but eventually you might find yourself drilling user down through Dashboard → UserProfile → UserAvatar, even though the intermediate components don't actually care about that data. This is prop drilling.

As applications grow, we often want a centralized place where shared application state can live. Components don't need to pass the state through every intermediate layer — they can subscribe to exactly what they need. That's the basic idea behind state management.

Redux: The Structured Approach

Redux is based around a centralized store and a predictable data flow. A component dispatches an action, the reducer determines how state changes, and the component re-renders with the new state.

dispatch({ type: "counter/increment" });

This structure is one of Redux's biggest strengths — it makes state changes explicit and predictable. But that predictability comes with concepts you need to understand before the whole system clicks.

The Redux Learning Curve

The difficult part isn't writing the code. It's understanding how all the pieces connect: Store → Actions → Reducers → Dispatch → Selectors → Middleware → Redux Toolkit → createSlice → configureStore → Provider → useSelector → useDispatch → createAsyncThunk → pending / fulfilled / rejected → extraReducers → TypeScript types for all of it.

Redux isn't necessarily difficult because each API is complicated. It can feel difficult because there are several concepts to understand before the whole system clicks.

A Small Redux Example

// counterSlice.ts
import { createSlice } from "@reduxjs/toolkit";

const counterSlice = createSlice({
  name: "counter",
  initialState: { value: 0 },
  reducers: {
    increment(state) { state.value += 1; },
    decrement(state) { state.value -= 1; },
  },
});

export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;
// Counter.tsx
import { useDispatch, useSelector } from "react-redux";
import { increment, decrement } from "./counterSlice";

function Counter() {
  const count = useSelector((state) => state.counter.value);
  const dispatch = useDispatch();

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

Zustand: A Game Changer

Zustand is a lightweight state-management library for React. Its API is deliberately small and hook-oriented. You create a store, put state and actions inside it, and consume the store directly from React components. No Provider. No configureStore. No separate action creators.

import { create } from "zustand";

interface CounterStore {
  count: number;
  increment: () => void;
  decrement: () => void;
}

export const useCounterStore = create<CounterStore>()((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
}));
function Counter() {
  const count = useCounterStore((state) => state.count);
  const increment = useCounterStore((state) => state.increment);
  const decrement = useCounterStore((state) => state.decrement);

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

That's the entire connection.

Why Selectors Matter

When you write useCounterStore((state) => state.count), you're telling Zustand you're interested in count. Subscribing to the entire store causes the component to update on every state change. Atomic selections — one value at a time — keep re-renders tight.

Async Operations

In Zustand, async logic lives directly in the store as a plain async function. There is no special "async Redux equivalent" to learn before making a request.

fetchUsers: async () => {
  set({ loading: true, error: null });
  try {
    const response = await fetch("/api/users");
    const users = await response.json();
    set({ users, loading: false });
  } catch {
    set({ loading: false, error: "Something went wrong" });
  }
},

Redux Toolkit's createAsyncThunk is equally capable — it generates pending, fulfilled, and rejected lifecycle actions automatically. Redux gives you a very explicit state-transition model. Zustand gives you a more direct one.

Practical Comparison

AreaRedux ToolkitZustand
Mental modelStore + actions + reducersStore + state + actions
BoilerplateLow with RTK, but more structureVery low
Learning curveSteeperGentler
Provider requiredYes (React-Redux)No
Async workflowscreateAsyncThunk + middlewareAsync functions in actions
TypeScriptExcellentExcellent
DevToolsExcellentAvailable
ArchitectureMore opinionatedMore flexible
Large teamsStrong conventionsRequires team conventions

When I Would Choose Zustand

I would seriously consider Zustand when:

  • The application's global state is relatively straightforward (theme, auth UI, cart, filters, modals)
  • The team values minimal ceremony
  • You want a simple mental model — a developer can understand useStore((s) => s.count) without learning reducers first
  • You're building a small-to-medium React application
  • You want flexible architecture that can grow with slices and middleware

When I Would Choose Redux Toolkit

  • The application has complex shared state where many parts interact in complicated ways
  • The team needs strong, universal conventions
  • Debugging state transitions is important — Redux DevTools shine here
  • You're working in an existing Redux codebase
  • Your organization already has Redux expertise and conventions

One More Important Question

Before installing Zustand or Redux, ask: does this state actually need to be global? If the state belongs to one component or a small section of the tree, local React state may be exactly what you need. And if the main problem is fetching, caching, and synchronizing server data, a server-state library may be a better abstraction than either.

The mistake isn't choosing Redux. The mistake isn't choosing Zustand. The mistake is choosing a state-management library before understanding the problem you're trying to solve.

14 min read