zustand
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 })))
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 |
More from tianyili/skills
react-architect-skills
React and TanStack Router folder structure and feature-module architecture. Use when writing, reviewing, or refactoring React code for structure, naming, colocation, encapsulation, and layer separation. Triggers on React folder structure, naming rules, module boundaries, or architecture decisions.
34vercel-react-only-best-practices
React performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React code to ensure optimal performance patterns. Triggers on tasks involving React components, data fetching, bundle optimization, or performance improvements.
21reduce-optional-params
Use when designing or refactoring functions with many optional params, configurable components, or when the user mentions optional params, function signatures, or API simplification. Reduces optional parameters in functions and components for cleaner APIs.
10react-table
Implements data tables using @tanstack/react-table v8 with project UI primitives. Use when adding or modifying tables, column configs, sorting, filtering, row selection, or virtualized table bodies.
4