---
title: "Zustand vs Jotai"
slug: "zustand-vs-jotai"
category: "comparisons"
tags: ["comparisons", "zustand", "jotai", "react", "state-management", "atomic-state"]
status: "stable"
last_updated: 2026-05-14
summary: "Use Zustand for flat, store-shaped state; use Jotai when state is naturally atomic and derivation graphs are the primary access pattern."
related: ["[[comparisons/zustand-vs-redux]]", "[[frontend/react-state-management]]", "[[frontend/react]]", "[[comparisons/tanstack-query-vs-swr]]", "[[comparisons/server-vs-client-components]]", "[[frontend/react-hooks]]", "[[frontend/index]]"]
---

## Overview

Use Zustand when your client state is a flat object with a few properties and actions, such as auth state, UI flags, or a shopping cart. Use Jotai when state is naturally atomic: independent pieces that compose into derived values, graph-like subscriptions, or fine-grained re-render control across many components. Both live in the same weight class (under 3 KB gzipped); the choice is structural, not a performance bet. See [[frontend/react-state-management]] for where each fits in the state hierarchy.

## When Zustand wins

Zustand is the right choice when state has a clear store shape.

- Store metaphor: Zustand is a module-scoped store. Define state and actions together in one `create` call. Components subscribe to slices and re-render only when that slice changes.
- Simplicity: the entire API is one `create` function and one `useStore` hook. New contributors learn it in minutes.
- Actions colocated: mutations live in the store definition, not scattered in components. This makes state transitions easy to audit.
- Middleware: `persist` (localStorage), `devtools` (Redux DevTools), `immer`, and `subscribeWithSelector` are opt-in and well-maintained.
- No provider for most uses: stores are plain JavaScript modules. Import and use. Provider wrapping is only needed for SSR per-request isolation.
- Suitable for: auth state, theme, modal state, form dirty tracking, multi-step wizard state, cart contents.

```ts
const useCart = create<CartStore>((set) => ({
  items: [],
  add: (item) => set((s) => ({ items: [...s.items, item] })),
  clear: () => set({ items: [] }),
}));
```

## When Jotai wins

Jotai is the right choice when state is naturally decomposed into independent atoms with derived relationships.

- Atom model: each piece of state is an atom. Components subscribe to exactly the atoms they need; unrelated atoms changing does not trigger re-renders.
- Derivation graphs: `atom((get) => get(priceAtom) * get(quantityAtom))` creates a derived atom that recalculates only when its inputs change. Complex derived state is easy to compose and test in isolation.
- Fine-grained subscriptions: in large component trees where many components read different parts of state, Jotai avoids the selector boilerplate that Zustand requires for the same effect.
- Async atoms: `atomWithQuery` (Jotai TanStack Query integration) and `loadable` wrap async state naturally. The result is a first-class async atom rather than a loading flag on a store.
- Suspense-first: Jotai integrates with React Suspense for async atom loading. Components read atoms with `useAtomValue` and suspend automatically.
- Suitable for: spreadsheet-like UIs, form fields with cross-field validation, data tables with per-row state, editor panels with many independent controls.

```ts
const priceAtom = atom(100);
const quantityAtom = atom(1);
const totalAtom = atom((get) => get(priceAtom) * get(quantityAtom));
```

## Trade-offs at a glance

| Dimension                  | Zustand                                   | Jotai                                          |
| -------------------------- | ----------------------------------------- | ---------------------------------------------- |
| Mental model               | Store (flat object + actions)             | Atoms (individual reactive values)             |
| Bundle size                | ~1 KB                                     | ~3 KB                                          |
| Re-render control          | Selector per component                    | Per-atom subscription; very fine-grained       |
| Derived state              | Selectors (may need memoization)          | Derived atoms (automatic)                      |
| Async state                | Middleware or external (TanStack Query)   | Built-in async atoms, Suspense integration     |
| Provider required          | Only for SSR                              | `Provider` for isolation; can also be global   |
| DevTools                   | Redux DevTools via middleware             | Jotai DevTools extension                       |
| Colocated actions          | Yes (in store definition)                 | Via `atomWithReducer` or atom families         |
| Boilerplate                | Very low                                  | Low; can grow with atom families               |
| Learning curve             | Very low                                  | Low; concept of atom families takes adjustment |
| TypeScript                 | Excellent inference                       | Excellent inference                            |

## Migration cost

Zustand to Jotai migration is not a codemod task. The two models are different enough that migration requires intentional redesign of the state shape.

- Identify atoms: each top-level property in a Zustand store becomes a candidate atom. Actions become functions that call `set` on the relevant atoms.
- Derived values map naturally to derived atoms. This is usually where Jotai pays off most visibly.
- Per-component: swap `useStore(s => s.value)` for `useAtomValue(valueAtom)`. Actions swap to `useSetAtom(setValueAtom)`.
- Effort: one engineer-day per 5 to 10 Zustand store files, assuming no complex middleware.

Both libraries coexist in the same app; migrating incrementally is safe.

## Recommendation

- New app with standard UI state (auth, modals, cart): Zustand.
- New app with complex derived state, spreadsheet-like UIs, or many independent subscriptions: Jotai.
- Existing Zustand app that is working: keep it. Do not migrate for its own sake.
- Pair either library with TanStack Query for server state; see [[comparisons/tanstack-query-vs-swr]].
- When unsure: Zustand first. Its simpler mental model pays off in most cases.

## Related

- [[comparisons/zustand-vs-redux]]
- [[frontend/react-state-management]]
- [[frontend/react]]
- [[comparisons/tanstack-query-vs-swr]]
- [[comparisons/server-vs-client-components]]
- [[frontend/react-hooks]]
- [[frontend/index]]
