
"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.
More posts

Authentication with better-auth in Next.js: A First-Hand Setup
The exact setup behind this site's own admin login: server config, the Next.js catch-all route handler, the client, and the two-layer middleware + layout protection that actually secures /admin.

Recharts in Next.js + Tailwind: Theming and Custom Styling
Making Recharts feel native to a Tailwind design system: correct Client Component boundaries, wiring colors to CSS variables for automatic dark mode, custom tooltips/legends, and handling RTL locales.