Zustand
A tiny, fast state management library for React. No boilerplate, no providers, no reducers — just a hook-based store that works.
Quick Verdict
Zustand is the state management library React should have shipped with. Create a store in 5 lines, use it anywhere with a hook, done. No Provider wrapping, no reducers, no action constants. The fastest path from "I need global state" to "it works in production."
When to use it: Client-side state that needs to be shared across components (modals, filters, UI toggles, cart state).
When not to: Server data (use TanStack Query), data that only lives in one component (use useState).
Best For
- UI state — modal open/close, sidebar state, filter selections, dark mode toggle
- Solo developers — 5 lines to create a store, zero configuration
- AI-assisted development — the API is small enough that LLMs get it right on the first try
- Next.js App Router — works alongside server components without wrapping your app in a Provider
Avoid If
- You need Redux DevTools time-travel debugging (Zustand's integration is basic)
- You have 5+ frontend developers who need a shared, enforced state architecture
- Your "state" is actually server data — that belongs in TanStack Query, not Zustand
- You need complex async state machines — Redux Toolkit is better designed for that
Why People Choose It
Redux fatigue drove the ecosystem toward simpler alternatives. Zustand gained adoption because it eliminated every pain point: no Provider hierarchy, no switch-case reducers, no action constants, no middleware configuration. You call create(), get a hook, and you're done.
Performance is also compelling. Zustand only re-renders components that subscribe to the specific slice of state they use — no React.memo gymnastics required.
Hidden Costs
Zero direct cost. The real "cost" is misuse: developers reach for Zustand when they need server state management (TanStack Query), and then fight stale data and manual cache invalidation. Keep the distinction clear: Zustand for client interaction state, TanStack Query/SWR for server data.
Correct vs Cargo-Culted Patterns
Wrong — subscribing to full store:
// ❌ Re-renders on every state change, not just your data
const store = useAppStore()Right — use selectors:
// ✅ Only re-renders when count changes
const count = useAppStore((state) => state.count)Wrong — server data in Zustand:
// ❌ Manual fetch, manual invalidation, stale data bugs
const useProjectStore = create((set) => ({
projects: [],
fetchProjects: async () => {
const data = await fetch('/api/projects').then(r => r.json())
set({ projects: data })
}
}))Right — TanStack Query for server data:
// ✅ Caching, background refetch, loading states — all handled
const { data: projects } = useQuery({ queryKey: ['projects'], queryFn: fetchProjects })Wrong — mega-store:
// ❌ One huge store for everything
const useAppStore = create(...) // contains auth, cart, ui, users, settings...Right — domain-split stores:
// ✅ Separate concerns, separate re-renders
const useAuthStore = create(...)
const useCartStore = create(...)
const useUIStore = create(...)AI Coding Notes
Zustand's small API produces accurate AI-generated code. Common errors to watch for:
- Provide the TypeScript interface first, then ask for the store — cleaner output
- Specify selector pattern explicitly: "always use selectors, never subscribe to full store"
- For nested state updates: ask for Immer middleware if state is deeply nested
Common AI Mistakes
const store = useStore()— subscribes to everything, re-renders on every change. Always use selectors.- Server data in Zustand —
fetchProjects()action in a Zustand store. Use TanStack Query instead. - Single mega-store — one
useAppStorefor everything. Split by domain. - Missing
persistmiddleware — state that should survive page refreshes (theme, sidebar state) gets lost on navigation. - Middleware overload — adding devtools + immer + persist to every store by default. Start with none, add when needed.
Start With / Grow Into / Avoid Until Needed
Start with Zustand for any client-side global state need. The API takes 10 minutes to learn.
Grow into the immer middleware for deeply nested state updates — prevents spread-hell.
Avoid until needed: persist middleware (only for state that must survive refreshes), devtools middleware (only when you're actively debugging state), subscribeWithSelector (advanced use case).
Migration Implications
Low pain. Migrating from Zustand to Redux Toolkit (if your team grows) means rewriting stores as slices and actions as dispatch calls. The state shape stays the same, the API changes. Allow 1–3 days per complex store.