zustand
Zustand State Management
Overview
Zustand v5 is a lightweight global state manager for React built on useSyncExternalStore. Requires React 18+ (uses useSyncExternalStore internally). When using createWithEqualityFn, install use-sync-external-store as a peer dependency. It provides type-safe stores, atomic selectors for minimal re-renders, composable middleware (persist, devtools, immer, subscribeWithSelector), and a slices pattern for large applications. Use Zustand for client-only global state; use TanStack Query for server-fetched data.
When to use: Client-side global state, persistent user preferences, complex multi-domain stores, cross-component state sharing, vanilla (non-React) state management.
When NOT to use: Server state with caching needs (use TanStack Query), single-component state (use useState), simple prop drilling scenarios.
Quick Reference
| Pattern | API | Key Points |
|---|---|---|
| Basic store | create<T>()((set) => ({...})) |
Double parentheses required for TS |
| Atomic selector | useStore(state => state.bears) |
Only re-renders when selected value changes |
| Multiple values | useShallow(state => ({a, b})) |
Import from zustand/react/shallow |
| Persist | persist(fn, { name }) |
localStorage with SSR hydration handling |
| Devtools | devtools(fn, { name }) |
Redux DevTools integration |
| SubscribeWithSelector | subscribeWithSelector(fn) |
Subscribe to state slices outside React |
| Middleware order | devtools(persist(fn)) |
Outer to inner wrapping |
| Slices pattern | StateCreator<Combined, [], [], Slice> |
Split store by domain |
| SSR provider | createStore + React Context |
Per-request stores prevent data leaks |
| Immer | immer(fn) |
Safe nested state mutations |
| Vanilla store | createStore from zustand/vanilla |
Use outside React |
| Reset store | set(store.getInitialState()) |
Use getInitialState() for reliable reset |
| Derived values | useStore(s => s.items.length) |
Compute in selector |
| Auto selectors | createSelectors(store) |
Generate store.use.field() hooks automatically |
Common Mistakes
| Mistake | Correct Pattern |
|---|---|
Using create<T>(...) with single parentheses |
Use create<T>()(...) double parentheses for correct TypeScript middleware inference |
Selecting full state object with useStore(state => state) |
Use atomic selectors like state.bears or useShallow to avoid unnecessary re-renders |
Importing useShallow from zustand/shallow |
Import from zustand/react/shallow; zustand/shallow exports the plain shallow function only |
| Creating new object references in selectors causing infinite loops | Use separate primitive selectors or wrap with useShallow from zustand/react/shallow |
| Using a global singleton store with SSR or Next.js | Use the StoreContext provider pattern with createStore to prevent data leaks between requests |
| Tracking initial state manually for reset | Use store.getInitialState() which Zustand provides automatically |
| Using Zustand for server-fetched data that needs caching and revalidation | Use TanStack Query for server state; reserve Zustand for client-only global state |
Delegation
- Store architecture and slices design: Use
Planagent to define domain boundaries, slice interfaces, and middleware composition order - Hydration and SSR debugging: Use
Taskagent to diagnose persist middleware issues, hydration mismatches, and Next.js provider setup - Migration from v4 to v5: Use
Exploreagent to find deprecated import paths, single-parentheses patterns, and legacy middleware usage - Testing strategy: Use
Taskagent to set up store mocks and test isolation patterns