Overview
Use Zustand when client state is one store with actions (auth, UI flags, cart, wizard); use Jotai when state is many independent atoms that derive from each other. As of 2026-10-01 Zustand is at 5.0 and Jotai at 3.0 (ESM-only; atomFamily moved to the separate jotai-family package and loadable and the jotai/babel plugin were removed). Both are tiny; the choice is structural.
Comparison
| Dimension | Zustand 5 | Jotai 3 |
|---|---|---|
| Model | One store: state plus actions | Atoms composed into derived atoms |
| Subscriptions | Selector per component | Per-atom |
| Derived state | Selectors | Derived atoms with automatic dependency tracking |
| Provider | Not needed; use a per-request store for SSR | Optional Provider for isolation |
| Async | Plain async functions in actions; TanStack Query for server data | Async atoms with Suspense |
| Middleware | persist, devtools, immer, subscribeWithSelector | Utilities such as atomWithStorage |
| Size | Under 1 KB gzipped | A few KB |
Pick Zustand when
Pick Zustand for state that has an obvious store shape.
- State and the actions that change it live together, which makes transitions easy to audit.
- No provider is needed for client-only apps. In Next.js, create the store per request inside a provider so state is not shared between users.
- In Zustand 5, a selector that returns a new object or array each call re-renders endlessly; wrap it in
useShallow.
import { create } from "zustand";
import { useShallow } from "zustand/shallow";
const useCart = create<CartStore>()((set) => ({
items: [],
add: (item) => set((s) => ({ items: [...s.items, item] })),
clear: () => set({ items: [] }),
}));
const { items, add } = useCart(useShallow((s) => ({ items: s.items, add: s.add })));Pick Jotai when
Pick Jotai for graph-shaped state where components read different slices.
- Derived values compute from other atoms and update only when inputs change:
atom((get) => get(price) * get(qty)). - Spreadsheet-like screens, forms with cross-field rules, and per-row state where many components subscribe to different pieces.
- Per-item state previously built with
atomFamilynow needs thejotai-familypackage in Jotai 3.
Choose and migrate
Start with Zustand when unsure; its model is the simpler one. Keep server data in TanStack Query either way (tanstack-query-vs-swr). The libraries can coexist, so migrate one feature at a time: each top-level store property becomes a candidate atom, derived selectors become derived atoms, and useStore(selector) becomes useAtomValue(atom). Do not migrate a working app for style alone.
FAQ
Should I use Zustand or Jotai?
Use Zustand 5 when client state is one store with actions, such as auth, UI flags, a cart, or a wizard. Use Jotai 3 when state is many independent atoms that derive from each other; start with Zustand when unsure.
Why does my Zustand selector cause an infinite re-render?
In Zustand 5, a selector that returns a new object or array each call re-renders endlessly. Wrap it in useShallow from zustand/shallow.
Where did atomFamily go in Jotai 3?
Jotai 3 moved atomFamily to the separate jotai-family package and removed loadable and the jotai/babel plugin. Jotai 3 is also ESM-only.
Do Zustand and Jotai need a Provider?
Zustand needs no provider for client-only apps, but in Next.js create the store per request inside a provider so state is not shared between users. Jotai’s Provider is optional and used for isolation.