Zustand Patterns
Modern state management with Zustand 5.x - lightweight, TypeScript-first, no boilerplate.
Overview
- Global state without Redux complexity
- Shared state across components without prop drilling
- Persisted state with localStorage/sessionStorage
- Computed/derived state with selectors
- State that needs middleware (logging, devtools, persistence)
Core Patterns
1. Basic Store with TypeScript
typescript
1import { create } from 'zustand';
2
3interface BearState {
4 bears: number;
5 increase: (by: number) => void;
6 reset: () => void;
7}
8
9const useBearStore = create<BearState>()((set) => ({
10 bears: 0,
11 increase: (by) => set((state) => ({ bears: state.bears + by })),
12 reset: () => set({ bears: 0 }),
13}));
2. Slices Pattern (Modular Stores)
typescript
1import { create, StateCreator } from 'zustand';
2
3// Auth slice
4interface AuthSlice {
5 user: User | null;
6 login: (user: User) => void;
7 logout: () => void;
8}
9
10const createAuthSlice: StateCreator<AuthSlice & CartSlice, [], [], AuthSlice> = (set) => ({
11 user: null,
12 login: (user) => set({ user }),
13 logout: () => set({ user: null }),
14});
15
16// Cart slice
17interface CartSlice {
18 items: CartItem[];
19 addItem: (item: CartItem) => void;
20 clearCart: () => void;
21}
22
23const createCartSlice: StateCreator<AuthSlice & CartSlice, [], [], CartSlice> = (set) => ({
24 items: [],
25 addItem: (item) => set((state) => ({ items: [...state.items, item] })),
26 clearCart: () => set({ items: [] }),
27});
28
29// Combined store
30const useStore = create<AuthSlice & CartSlice>()((...a) => ({
31 ...createAuthSlice(...a),
32 ...createCartSlice(...a),
33}));
3. Immer Middleware (Immutable Updates)
typescript
1import { create } from 'zustand';
2import { immer } from 'zustand/middleware/immer';
3
4interface TodoState {
5 todos: Todo[];
6 addTodo: (text: string) => void;
7 toggleTodo: (id: string) => void;
8 updateNested: (id: string, subtaskId: string, done: boolean) => void;
9}
10
11const useTodoStore = create<TodoState>()(
12 immer((set) => ({
13 todos: [],
14 addTodo: (text) =>
15 set((state) => {
16 state.todos.push({ id: crypto.randomUUID(), text, done: false });
17 }),
18 toggleTodo: (id) =>
19 set((state) => {
20 const todo = state.todos.find((t) => t.id === id);
21 if (todo) todo.done = !todo.done;
22 }),
23 updateNested: (id, subtaskId, done) =>
24 set((state) => {
25 const todo = state.todos.find((t) => t.id === id);
26 const subtask = todo?.subtasks?.find((s) => s.id === subtaskId);
27 if (subtask) subtask.done = done;
28 }),
29 }))
30);
4. Persist Middleware
typescript
1import { create } from 'zustand';
2import { persist, createJSONStorage } from 'zustand/middleware';
3
4interface SettingsState {
5 theme: 'light' | 'dark';
6 language: string;
7 setTheme: (theme: 'light' | 'dark') => void;
8}
9
10const useSettingsStore = create<SettingsState>()(
11 persist(
12 (set) => ({
13 theme: 'light',
14 language: 'en',
15 setTheme: (theme) => set({ theme }),
16 }),
17 {
18 name: 'settings-storage',
19 storage: createJSONStorage(() => localStorage),
20 partialize: (state) => ({ theme: state.theme }), // Only persist theme
21 version: 1,
22 migrate: (persisted, version) => {
23 if (version === 0) {
24 // Migration logic
25 }
26 return persisted as SettingsState;
27 },
28 }
29 )
30);
5. Selectors (Prevent Re-renders)
typescript
1// ❌ BAD: Re-renders on ANY state change
2const { bears, fish } = useBearStore();
3
4// ✅ GOOD: Only re-renders when bears changes
5const bears = useBearStore((state) => state.bears);
6
7// ✅ GOOD: Shallow comparison for objects (Zustand 5.x)
8import { useShallow } from 'zustand/react/shallow';
9
10const { bears, fish } = useBearStore(
11 useShallow((state) => ({ bears: state.bears, fish: state.fish }))
12);
13
14// ✅ GOOD: Computed/derived state via selector
15const totalAnimals = useBearStore((state) => state.bears + state.fish);
16
17// ❌ BAD: Storing computed state
18const useStore = create((set) => ({
19 items: [],
20 total: 0, // Don't store derived values!
21 addItem: (item) => set((s) => ({
22 items: [...s.items, item],
23 total: s.total + item.price, // Sync issues!
24 })),
25}));
26
27// ✅ GOOD: Compute in selector
28const total = useStore((s) => s.items.reduce((sum, i) => sum + i.price, 0));
6. Async Actions
typescript
1interface UserState {
2 user: User | null;
3 loading: boolean;
4 error: string | null;
5 fetchUser: (id: string) => Promise<void>;
6}
7
8const useUserStore = create<UserState>()((set) => ({
9 user: null,
10 loading: false,
11 error: null,
12 fetchUser: async (id) => {
13 set({ loading: true, error: null });
14 try {
15 const user = await api.getUser(id);
16 set({ user, loading: false });
17 } catch (error) {
18 set({ error: error.message, loading: false });
19 }
20 },
21}));
typescript
1import { create } from 'zustand';
2import { devtools } from 'zustand/middleware';
3
4const useStore = create<State>()(
5 devtools(
6 (set) => ({
7 // ... state and actions
8 }),
9 { name: 'MyStore', enabled: process.env.NODE_ENV === 'development' }
10 )
11);
Quick Reference
typescript
1// ✅ Create typed store with double-call pattern
2const useStore = create<State>()((set, get) => ({ ... }));
3
4// ✅ Use selectors for all state access
5const count = useStore((s) => s.count);
6
7// ✅ Use useShallow for multiple values (Zustand 5.x)
8const { a, b } = useStore(useShallow((s) => ({ a: s.a, b: s.b })));
9
10// ✅ Middleware order: immer → subscribeWithSelector → devtools → persist
11create(persist(devtools(immer((set) => ({ ... })))))
12
13// ❌ Never destructure entire store
14const store = useStore(); // Re-renders on ANY change
15
16// ❌ Never store server state (use TanStack Query instead)
17const useStore = create((set) => ({ users: [], fetchUsers: async () => ... }));
Key Decisions
| Decision | Option A | Option B | Recommendation |
|---|
| State structure | Single store | Multiple stores | Slices in single store - easier cross-slice access |
| Nested updates | Spread operator | Immer middleware | Immer for deeply nested state (3+ levels) |
| Persistence | Manual localStorage | persist middleware | persist middleware with partialize |
| Multiple values | Multiple selectors | useShallow | useShallow for 2-5 related values |
| Server state | Zustand | TanStack Query | TanStack Query - Zustand for client-only state |
| DevTools | Always on | Conditional | Conditional - enabled: process.env.NODE_ENV === 'development' |
Anti-Patterns (FORBIDDEN)
typescript
1// ❌ FORBIDDEN: Destructuring entire store
2const { count, increment } = useStore(); // Re-renders on ANY state change
3
4// ❌ FORBIDDEN: Storing derived/computed state
5const useStore = create((set) => ({
6 items: [],
7 total: 0, // Will get out of sync!
8}));
9
10// ❌ FORBIDDEN: Storing server state
11const useStore = create((set) => ({
12 users: [], // Use TanStack Query instead
13 fetchUsers: async () => { ... },
14}));
15
16// ❌ FORBIDDEN: Mutating state without Immer
17set((state) => {
18 state.items.push(item); // Breaks reactivity!
19 return state;
20});
21
22// ❌ FORBIDDEN: Using deprecated shallow import
23import { shallow } from 'zustand/shallow'; // Use useShallow from zustand/react/shallow
Integration with React Query
typescript
1// ✅ Zustand for CLIENT state (UI, preferences, local-only)
2const useUIStore = create<UIState>()((set) => ({
3 sidebarOpen: false,
4 theme: 'light',
5 toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
6}));
7
8// ✅ TanStack Query for SERVER state (API data)
9function Dashboard() {
10 const sidebarOpen = useUIStore((s) => s.sidebarOpen);
11 const { data: users } = useQuery({ queryKey: ['users'], queryFn: fetchUsers });
12 // Zustand: UI state | TanStack Query: server data
13}
tanstack-query-advanced - Server state management (use with Zustand for client state)
form-state-patterns - Form state (React Hook Form vs Zustand for forms)
react-server-components-framework - RSC hydration considerations with Zustand
Capability Details
store-creation
Keywords: zustand, create, store, typescript, state
Solves: Setting up type-safe Zustand stores with proper TypeScript inference
slices-pattern
Keywords: slices, modular, split, combine, StateCreator
Solves: Organizing large stores into maintainable, domain-specific slices
middleware-stack
Keywords: immer, persist, devtools, middleware, compose
Solves: Combining middleware in correct order for immutability, persistence, and debugging
selector-optimization
Keywords: selector, useShallow, re-render, performance, memoization
Solves: Preventing unnecessary re-renders with proper selector patterns
persistence-migration
Keywords: persist, localStorage, sessionStorage, migrate, version
Solves: Persisting state with schema migrations between versions
References
references/middleware-composition.md - Combining multiple middleware
scripts/store-template.ts - Production-ready store template
checklists/zustand-checklist.md - Implementation checklist