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

DimensionZustand 5Jotai 3
ModelOne store: state plus actionsAtoms composed into derived atoms
SubscriptionsSelector per componentPer-atom
Derived stateSelectorsDerived atoms with automatic dependency tracking
ProviderNot needed; use a per-request store for SSROptional Provider for isolation
AsyncPlain async functions in actions; TanStack Query for server dataAsync atoms with Suspense
Middlewarepersist, devtools, immer, subscribeWithSelectorUtilities such as atomWithStorage
SizeUnder 1 KB gzippedA 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 atomFamily now needs the jotai-family package 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.