zustand
Lightweight state management for React applications
$ npx docs2skills add zustand-state-managementZustand
Lightweight state management for React applications
What this skill does
Zustand is a small, fast, and scalable state management solution for React that eliminates boilerplate while providing a hooks-based API. Unlike Redux or Context API, it requires no providers, reducers, or complex setup - just create a store and use it anywhere in your component tree.
It solves critical React state management problems including the zombie child problem (stale closures), React concurrency issues, and context loss between mixed renderers. The library focuses on simplicity while handling edge cases that other state managers miss, making it ideal for both simple and complex applications.
Zustand's philosophy is "flux-like but not opinionated" - it provides enough convention to be explicit about state changes while staying flexible enough to adapt to any architecture.
Prerequisites
- React 16.8+ (hooks support)
- Node.js and npm/yarn
- TypeScript support available but optional
Quick start
npm install zustand
import { create } from 'zustand'
// Create store
const useBearStore = create((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
}))
// Use in components
function BearCounter() {
const bears = useBearStore((state) => state.bears)
return <h1>{bears} bears around here...</h1>
}
function Controls() {
const increasePopulation = useBearStore((state) => state.increasePopulation)
return <button onClick={increasePopulation}>one up</button>
}
Core concepts
Store as Hook: Every Zustand store is a React hook that can be called anywhere without providers.
Selective Subscriptions: Components subscribe only to specific state slices using selector functions, preventing unnecessary re-renders.
Immutable Updates: The set function merges state shallowly. For nested updates, return new objects or use immer middleware.
Actions in State: Actions are just functions stored alongside data in the same state object, keeping related logic together.
No Boilerplate: No action creators, dispatchers, or reducers required - just functions that call set or get.
Key API surface
// Store creation
const useStore = create((set, get) => ({ /* state */ }))
// State access with selector
const value = useStore(selector)
const value = useStore(selector, equalityFn)
// Direct state access (outside React)
const state = useStore.getState()
// Subscribe to changes (outside React)
const unsubscribe = useStore.subscribe(listener)
// Update state
set(partial)
set((state) => newState)
// Read current state in actions
get()
// Destroy store
useStore.destroy()
Common patterns
Basic state with actions:
const useStore = create((set, get) => ({
count: 0,
text: '',
increment: () => set((state) => ({ count: state.count + 1 })),
updateText: (text) => set({ text }),
}))
Async actions:
const useStore = create((set) => ({
users: [],
loading: false,
fetchUsers: async () => {
set({ loading: true })
const users = await api.getUsers()
set({ users, loading: false })
},
}))
Nested state updates:
const useStore = create((set) => ({
user: { name: '', settings: { theme: 'light' } },
updateTheme: (theme) => set((state) => ({
user: {
...state.user,
settings: { ...state.user.settings, theme }
}
})),
}))
Computed values:
const useStore = create((set, get) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
get total() {
return get().items.reduce((sum, item) => sum + item.price, 0)
},
}))
Slicing state for performance:
// Only re-render when count changes, not when text changes
const count = useStore((state) => state.count)
// Multiple values with shallow equality
const { count, increment } = useStore(
(state) => ({ count: state.count, increment: state.increment }),
shallow
)
Configuration
Custom equality function:
import { shallow } from 'zustand/shallow'
const data = useStore((state) => [state.a, state.b], shallow)
Middleware:
import { devtools, persist, subscribeWithSelector } from 'zustand/middleware'
const useStore = create(
devtools(
persist(
(set) => ({ /* state */ }),
{ name: 'my-store' }
)
)
)
TypeScript:
interface BearState {
bears: number
increase: (by: number) => void
}
const useBearStore = create<BearState>((set) => ({
bears: 0,
increase: (by) => set((state) => ({ bears: state.bears + by })),
}))
Best practices
- Use descriptive selector functions to minimize re-renders
- Keep actions close to related state in the same store
- Prefer multiple small stores over one large store for better performance
- Use
shallowequality for selecting multiple primitive values - Avoid storing derived state - compute it in selectors or use getters
- Use middleware like
persistfor localStorage integration - Enable
devtoolsmiddleware in development for debugging - Use TypeScript interfaces to define store shape
- Keep stores flat when possible to avoid nested update complexity
- Use
subscribeWithSelectormiddleware when you need to react to specific state changes outside components
Gotchas and common mistakes
Selector function identity: Creating new selector functions on every render causes unnecessary subscriptions. Extract selectors or use useCallback.
Mutating state directly: Never mutate state directly. Always use set() to trigger updates and re-renders.
Stale closures in async actions: Use get() inside async functions to access current state, not captured variables.
Nested object updates: set() merges shallowly. For nested updates, spread existing nested objects or use immer middleware.
Selecting entire state: useStore() without selector subscribes to all changes. Always use selectors for performance.
Equality checks: Objects and arrays need custom equality functions like shallow to prevent re-renders when contents are the same.
Server-side rendering: Stores created during SSR persist across requests. Use create() inside components or reset stores between requests.
Memory leaks: Manual subscriptions outside React need cleanup. Store the unsubscribe function and call it appropriately.
Actions in selectors: Don't select actions with other state if you want to use shallow equality - actions are stable references.
Middleware order matters: Apply middleware in correct order - devtools should typically be outermost, persist inner.