Zustand
Guidance for using Zustand (create, stores, middleware, and scaling patterns). No provider required.
Cursor rule
A file-scoped rule is provided at rules/zustand-stores.mdc. Copy it to your project’s .cursor/rules/ to get automatic reminders when editing store files.
When to Apply
- Adding or refactoring Zustand stores
- Persisting state (e.g. localStorage)
- Splitting a large store into slices
- User asks about Zustand, global state, or store structure
Store Basics
import { create } from 'zustand'
const useStore = create((set) => ({
count: 0,
inc: () => set((s) => ({ count: s.count + 1 })),
reset: () => set({ count: 0 }),
}))
- set(state | fn): Replace state or use updater
set((s) => ({ ... })). - get(): Read current state inside actions (e.g.
get().count). - Prefer updater form for derived updates:
set((s) => ({ count: s.count + 1 })).
Selectors and Re-renders
Select only what the component needs to avoid unnecessary re-renders.
// Bad – re-renders on any store change
const state = useStore()
// Good – re-renders only when count changes
const count = useStore((s) => s.count)
const inc = useStore((s) => s.inc)
Use shallow when selecting multiple fields:
import { useShallow } from 'zustand/react/shallow'
const { count, inc } = useStore(useShallow((s) => ({ count: s.count, inc: s.inc })))
In v5 this is a correctness requirement, not just a perf nicety: selectors must return
stable references, and a selector that returns a fresh object/array each call without
useShallow throws "Maximum update depth exceeded".
Middleware
Persist (e.g. localStorage)
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
const useStore = create(
persist(
(set) => ({
token: null,
setToken: (t) => set({ token: t }),
}),
{
name: 'app-storage',
storage: createJSONStorage(() => localStorage),
partialize: (s) => ({ token: s.token }), // optional: persist only part
},
),
)
For async storage (e.g. React Native), use a storage adapter that returns Promises; the store will rehydrate when the promise resolves.
Devtools
import { devtools } from 'zustand/middleware'
const useStore = create(
devtools(
(set) => ({ ... }),
{ name: 'MyStore' },
),
)
Combining Middleware
Order: store creator innermost, then devtools, then persist (so persist wraps the devtools-wrapped store).
create(
persist(
devtools((set) => ({ ... }), { name: 'AppStore' }),
{ name: 'app-storage' },
),
)
Slices Pattern (Scaling)
Split one store into slice creators; compose them in a single create call.
const createAuthSlice = (set) => ({
user: null,
login: (user) => set({ user }),
logout: () => set({ user: null }),
})
const createUiSlice = (set) => ({
theme: 'light',
setTheme: (theme) => set({ theme }),
})
const useStore = create((...a) => ({
...createAuthSlice(...a),
...createUiSlice(...a),
}))
Each slice receives (set, get, store) and returns a plain object; avoid naming collisions between slices.
Immer (Mutable-style Updates)
Use immer middleware when updaters are complex or nested.
import { immer } from 'zustand/middleware/immer'
const useStore = create(
immer((set) => ({
nested: { a: 1, b: { c: 2 } },
updateC: (v) => set((s) => {
s.nested.b.c = v
}),
})),
)
TypeScript
Type the store state and actions; infer hook types from create.
interface BearState {
bears: number
addBear: () => void
}
const useBearStore = create<BearState>()((set) => ({
bears: 0,
addBear: () => set((s) => ({ bears: s.bears + 1 })),
}))
Use create<State>()(...) so middlewares keep correct typing.
Vanilla (Outside React)
Same store can be used without React: useStore.getState(), useStore.setState(...), useStore.subscribe((state) => { ... }).
Anti-patterns
- Don’t put everything in one giant store; prefer slices or multiple stores by domain.
- Don’t select the whole store in components; use selectors or
useShallowfor multiple fields. - Don’t forget
partializewith persist when the store has non-serializable values (e.g. functions, class instances).
Summary
| Need | Use |
|---|---|
| Basic store | create((set) => ({ ... })) |
| Persist | persist(..., { name, storage }) |
| DevTools | devtools(...) |
| Nested updates | immer |
| Scale store | Slice functions + spread in create |
| Fewer re-renders | Selectors or useShallow |