Zustand
A minimalist global store: create, set with merging, selectors, persist.
Updated
What Zustand is
Zustand is a small (~1 KB) library for global state: a store created with a function, used like a hook. No provider, no string action types, no reducers — just state and the functions that change it.
import { create } from 'zustand'
type CartState = {
items: number[]
add: (id: number) => void
clear: () => void
}
export const useCart = create<CartState>()(set => ({
items: [],
add: id => set(state => ({ items: [...state.items, id] })),
clear: () => set({ items: [] }),
}))'use client'
const count = useCart(state => state.items.length) // a selector
const add = useCart(state => state.add)
<button onClick={() => add(product.id)}>Add</button>How it works
createreturns a hook. The store lives outside React; components subscribe to it.setmerges the new object into the existing state, at the first level — you don't have to copy everything.- A component re-renders only when the value returned by its selector changes.
Selectors — the most important detail
| Code | Re-renders when |
|---|---|
useCart() (no selector) |
anything in the store changes ✗ |
useCart(s => s.items.length) |
only when the count changes ✓ |
useCart(s => ({ a: s.a, b: s.b })) |
always — a new object on every render ✗ |
useCart(useShallow(s => ({ a: s.a, b: s.b }))) |
when a or b changes ✓ |
The rule: select as little as possible; for several values, several calls or useShallow.
Useful middleware
import { persist, devtools } from 'zustand/middleware'
export const useSettings = create<Settings>()(
persist(
set => ({ theme: 'light', setTheme: theme => set({ theme }) }),
{ name: 'settings' }, // the localStorage key
),
)| Middleware | What it does |
|---|---|
persist |
saves the store to localStorage / sessionStorage |
devtools |
connects to Redux DevTools |
immer |
"mutable" syntax, like in RTK |
Zustand vs Redux Toolkit
| Zustand | Redux Toolkit | |
|---|---|---|
| Code for a simple store | ~10 lines | a slice + a store + a provider + typed hooks |
| Provider | not needed | yes |
| Enforced structure | little — you decide | a lot — slices, actions |
| Server state | no (use TanStack Query) | RTK Query included |
| A good fit for | most apps | big apps, teams that want strict rules |
Zustand with Next.js
A module-level global store is shared on the server between requests. The rules:
- Use the store only in Client Components.
- Don't put user-specific data in it during server rendering; for that, create the store per request (
createStore+ Context) — the official guide describes the pattern. - With
persist, the value fromlocalStorageappears only after hydration.
Where it lives in FSD
A store tied to an entity → entities/cart/model/cart-store.ts. A global UI store → shared/model/ or the slice that owns it.
Summary
- Zustand =
create→ a hook;setmerges; no provider. - Small selectors;
useShallowfor several values. persistfor localStorage; in Next only on the client, watch out for per-user data.