Back to blog

State Management Compared: Redux Toolkit vs Zustand vs Jotai

July 24, 20263 min read
State Management Compared: Redux Toolkit vs Zustand vs Jotai

"Which state manager should I use?" is really three different questions wearing a trench coat: how much boilerplate am I willing to write, do I need time-travel debugging, and is my state actually global or just shared between a few nearby components? Redux Toolkit, Zustand, and Jotai answer those questions very differently.

Redux Toolkit — one centralized store, explicit everything

Redux Toolkit (RTK) is the modern, opinionated way to write Redux: less boilerplate than classic Redux, but the same core idea — a single store, actions describing what happened, reducers deciding how state changes.

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

const counterSlice = createSlice({
  name: "counter",
  initialState: { value: 0 },
  reducers: {
    increment: (state) => { state.value += 1; }, // Immer under the hood — this "mutation" is safe
    incrementBy: (state, action: { payload: number }) => { state.value += action.payload; },
  },
});

export const { increment, incrementBy } = counterSlice.actions;
export default counterSlice.reducer;

// In a component
const count = useSelector((state: RootState) => state.counter.value);
const dispatch = useDispatch();
dispatch(increment());

Zustand — a store that's just a hook

Zustand throws out the ceremony. A store is a function; components subscribe to just the slice they read, so re-renders stay minimal without extra memoization.

// store/useCounterStore.ts
import { create } from "zustand";

const useCounterStore = create<{ value: number; increment: () => void }>((set) => ({
  value: 0,
  increment: () => set((s) => ({ value: s.value + 1 })),
}));

// In a component — only re-renders when `value` changes
const count = useCounterStore((s) => s.value);
const increment = useCounterStore((s) => s.increment);

Jotai — state as atoms, not one big object

Jotai flips the model: instead of one store holding everything, state lives in small, independent "atoms" that compose. No selectors needed — a component reading an atom only re-renders when that atom changes.

// atoms/counter.ts
import { atom, useAtom } from "jotai";

const countAtom = atom(0);
const doubledAtom = atom((get) => get(countAtom) * 2); // derived atom, recomputes automatically

// In a component
const [count, setCount] = useAtom(countAtom);
const [doubled] = useAtom(doubledAtom);

Side by side

Redux Toolkit

Zustand

Jotai

Mental model

One global store, actions + reducers

A store is a hook

State as composable atoms

Boilerplate

Moderate (slices, but far less than classic Redux)

Minimal

Minimal

Bundle size (approx.)

~11kb (+ React-Redux)

~1kb

~3kb

DevTools / time travel

Excellent (Redux DevTools)

Basic, via middleware

Basic, via devtools util

Async logic

Built-in (createAsyncThunk / RTK Query)

Manual, or a middleware

Manual, atoms can be async natively

Fine-grained re-renders

Requires careful selectors

Automatic per-slice

Automatic per-atom

Best fit

Large apps, complex shared domain state, teams wanting strict structure

Small-to-medium apps, quick to adopt, minimal ceremony

Highly interdependent, derived UI state (forms, filters, wizards)

Which one, actually

  • Reach for Redux Toolkit when the state is genuinely cross-cutting, the team is larger than a couple of people, and you want RTK Query's caching for server data too.

  • Reach for Zustand when you want global state without ceremony — most side projects and mid-size apps land here.

  • Reach for Jotai when your state is naturally a graph of small, derived values — form wizards, filter panels, anything where fields depend on each other.

  • And before reaching for any of them: check whether it's actually global state, or just props/context passed a couple of levels — not everything needs a store.

The best state manager is the smallest one that makes the current problem easy to reason about. It's fine — good, even — to use different tools for different parts of the same app.