# Zustand Zustand is a small, fast, and scalable state management solution for React applications. It uses simplified flux principles and provides a comfy, hook-based API that eliminates boilerplate code. Unlike Redux, Zustand doesn't require wrapping your app in context providers, making it simpler to set up and use while still delivering powerful state management capabilities. The library is designed to handle common React pitfalls including the zombie child problem, React concurrency issues, and context loss between mixed renderers. Zustand works both with React (via hooks) and vanilla JavaScript, supports TypeScript out of the box, and offers middleware for persistence, devtools integration, and immutable state updates through Immer. ## Core APIs ### create - Create a React Store Hook The `create` function creates a React hook with attached API utilities for state management. It takes a state creator function that receives `set`, `get`, and `store` arguments and returns an object containing your state and actions. ```typescript import { create } from 'zustand' interface BearState { bears: number increasePopulation: () => void removeAllBears: () => void updateBears: (newBears: number) => void } const useBearStore = create()((set) => ({ bears: 0, increasePopulation: () => set((state) => ({ bears: state.bears + 1 })), removeAllBears: () => set({ bears: 0 }), updateBears: (newBears) => set({ bears: newBears }), })) // Usage in React components function BearCounter() { const bears = useBearStore((state) => state.bears) const increasePopulation = useBearStore((state) => state.increasePopulation) return (

{bears} bears around here

) } // Access state outside React components const currentBears = useBearStore.getState().bears useBearStore.setState({ bears: 10 }) const unsubscribe = useBearStore.subscribe((state) => console.log('State changed:', state)) ``` ### createStore - Create a Vanilla Store The `createStore` function creates a store without React hooks, suitable for use outside React or when you need to share state across different frameworks. It returns a store object with `getState`, `setState`, `subscribe`, and `getInitialState` methods. ```typescript import { createStore } from 'zustand/vanilla' type CounterState = { count: number } type CounterActions = { increment: () => void decrement: () => void reset: () => void } type CounterStore = CounterState & CounterActions const counterStore = createStore()((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })), reset: () => set({ count: 0 }), })) // Reading state const currentCount = counterStore.getState().count // Updating state counterStore.getState().increment() counterStore.setState({ count: 100 }) // Subscribing to changes const unsubscribe = counterStore.subscribe((state) => { console.log('Count changed to:', state.count) }) // Get initial state for comparison const initialState = counterStore.getInitialState() ``` ### useStore - Use Vanilla Store in React The `useStore` hook allows you to consume a vanilla store within React components. It takes a store instance and an optional selector function to extract specific state slices. ```typescript import { createStore, useStore } from 'zustand' import { createContext, useContext, useState, ReactNode } from 'react' type PositionStore = { position: { x: number; y: number } setPosition: (pos: { x: number; y: number }) => void } const createPositionStore = () => createStore()((set) => ({ position: { x: 0, y: 0 }, setPosition: (position) => set({ position }), })) // Create context for scoped stores const PositionStoreContext = createContext | null>(null) function PositionStoreProvider({ children }: { children: ReactNode }) { const [store] = useState(() => createPositionStore()) return ( {children} ) } function usePositionStore(selector: (state: PositionStore) => U) { const store = useContext(PositionStoreContext) if (!store) throw new Error('Missing PositionStoreProvider') return useStore(store, selector) } function MovingDot() { const position = usePositionStore((state) => state.position) const setPosition = usePositionStore((state) => state.setPosition) return (
setPosition({ x: e.clientX, y: e.clientY })} style={{ width: '100vw', height: '100vh', position: 'relative' }} >
) } ``` ### useShallow - Optimize Re-renders with Shallow Comparison The `useShallow` hook creates a memoized selector that uses shallow comparison to prevent unnecessary re-renders when selecting objects or arrays from state. ```typescript import { create } from 'zustand' import { useShallow } from 'zustand/react/shallow' interface StoreState { nuts: number honey: number treats: Record addNut: () => void addHoney: () => void } const useStore = create()((set) => ({ nuts: 0, honey: 0, treats: { cookies: 5, candy: 3 }, addNut: () => set((state) => ({ nuts: state.nuts + 1 })), addHoney: () => set((state) => ({ honey: state.honey + 1 })), })) function Component() { // Without useShallow - re-renders on ANY state change // const { nuts, honey } = useStore((state) => ({ nuts: state.nuts, honey: state.honey })) // With useShallow - only re-renders when nuts or honey actually change const { nuts, honey } = useStore( useShallow((state) => ({ nuts: state.nuts, honey: state.honey })) ) // Array selection with shallow comparison const [nutsCount, honeyCount] = useStore( useShallow((state) => [state.nuts, state.honey]) ) // Object keys with shallow comparison const treatTypes = useStore( useShallow((state) => Object.keys(state.treats)) ) return (

Nuts: {nuts}, Honey: {honey}

Treat types: {treatTypes.join(', ')}

) } ``` ## Middleware ### persist - Persist State to Storage The `persist` middleware automatically saves state to storage (localStorage by default) and rehydrates it on page load. It supports custom storage backends, partial persistence, versioning, and migrations. ```typescript import { create } from 'zustand' import { persist, createJSONStorage } from 'zustand/middleware' interface AuthState { user: { id: string; name: string } | null token: string | null theme: 'light' | 'dark' login: (user: { id: string; name: string }, token: string) => void logout: () => void setTheme: (theme: 'light' | 'dark') => void } const useAuthStore = create()( persist( (set) => ({ user: null, token: null, theme: 'light', login: (user, token) => set({ user, token }), logout: () => set({ user: null, token: null }), setTheme: (theme) => set({ theme }), }), { name: 'auth-storage', storage: createJSONStorage(() => localStorage), // Only persist specific fields partialize: (state) => ({ user: state.user, theme: state.theme }), // Version for migrations version: 1, // Migrate from older versions migrate: (persistedState: any, version) => { if (version === 0) { // Migration logic from v0 to v1 persistedState.theme = persistedState.darkMode ? 'dark' : 'light' delete persistedState.darkMode } return persistedState }, // Custom rehydration callback onRehydrateStorage: () => (state, error) => { if (error) { console.error('Failed to rehydrate:', error) } else { console.log('Rehydration complete') } }, } ) ) // Manual rehydration control const useManualStore = create( persist( (set) => ({ count: 0 }), { name: 'manual-storage', skipHydration: true, // Don't auto-hydrate } ) ) // Later, manually trigger rehydration useManualStore.persist.rehydrate() ``` ### devtools - Redux DevTools Integration The `devtools` middleware enables time-travel debugging using the Redux DevTools browser extension. It logs actions with custom names and supports filtering. ```typescript import { create } from 'zustand' import { devtools } from 'zustand/middleware' interface TodoState { todos: Array<{ id: number; text: string; done: boolean }> addTodo: (text: string) => void toggleTodo: (id: number) => void removeTodo: (id: number) => void } const useTodoStore = create()( devtools( (set) => ({ todos: [], addTodo: (text) => set( (state) => ({ todos: [...state.todos, { id: Date.now(), text, done: false }], }), undefined, 'todos/add' // Action name for DevTools ), toggleTodo: (id) => set( (state) => ({ todos: state.todos.map((todo) => todo.id === id ? { ...todo, done: !todo.done } : todo ), }), undefined, { type: 'todos/toggle', id } // Action with payload ), removeTodo: (id) => set( (state) => ({ todos: state.todos.filter((todo) => todo.id !== id), }), undefined, 'todos/remove' ), }), { name: 'TodoStore', enabled: process.env.NODE_ENV === 'development', // Filter out internal actions from DevTools actionsDenylist: ['internal/.*'], } ) ) // Cleanup DevTools connection when done // useTodoStore.devtools.cleanup() ``` ### immer - Immutable Updates with Mutable Syntax The `immer` middleware allows you to write state updates using mutable syntax while automatically producing immutable state. This simplifies complex nested state updates. ```typescript import { create } from 'zustand' import { immer } from 'zustand/middleware/immer' interface UserState { user: { profile: { name: string email: string settings: { notifications: boolean theme: string } } posts: Array<{ id: number; title: string; likes: number }> } updateName: (name: string) => void toggleNotifications: () => void addPost: (title: string) => void likePost: (id: number) => void } const useUserStore = create()( immer((set) => ({ user: { profile: { name: 'John', email: 'john@example.com', settings: { notifications: true, theme: 'light', }, }, posts: [], }, // Direct mutation syntax - Immer handles immutability updateName: (name) => set((state) => { state.user.profile.name = name }), toggleNotifications: () => set((state) => { state.user.profile.settings.notifications = !state.user.profile.settings.notifications }), addPost: (title) => set((state) => { state.user.posts.push({ id: Date.now(), title, likes: 0, }) }), likePost: (id) => set((state) => { const post = state.user.posts.find((p) => p.id === id) if (post) post.likes++ }), })) ) ``` ### subscribeWithSelector - Granular Subscriptions The `subscribeWithSelector` middleware enables subscribing to specific state slices with custom equality functions, useful for external state management and side effects. ```typescript import { createStore } from 'zustand/vanilla' import { subscribeWithSelector } from 'zustand/middleware' import { shallow } from 'zustand/shallow' interface GameState { score: number level: number player: { x: number; y: number } addScore: (points: number) => void nextLevel: () => void movePlayer: (x: number, y: number) => void } const gameStore = createStore()( subscribeWithSelector((set) => ({ score: 0, level: 1, player: { x: 0, y: 0 }, addScore: (points) => set((state) => ({ score: state.score + points })), nextLevel: () => set((state) => ({ level: state.level + 1 })), movePlayer: (x, y) => set({ player: { x, y } }), })) ) // Subscribe to specific state slice const unsubscribeScore = gameStore.subscribe( (state) => state.score, (score, prevScore) => { console.log(`Score changed: ${prevScore} -> ${score}`) if (score >= 100) console.log('Achievement unlocked!') } ) // Subscribe with shallow equality for objects const unsubscribePlayer = gameStore.subscribe( (state) => state.player, (player) => console.log('Player moved to:', player), { equalityFn: shallow } ) // Fire immediately on subscribe const unsubscribeLevel = gameStore.subscribe( (state) => state.level, (level) => console.log('Current level:', level), { fireImmediately: true } ) ``` ## Patterns ### Slices Pattern - Modular Store Organization The slices pattern allows you to split a large store into smaller, focused modules that can be combined together. Each slice manages its own state and actions. ```typescript import { create, StateCreator } from 'zustand' import { devtools, persist } from 'zustand/middleware' // Define slice types interface BearSlice { bears: number addBear: () => void eatFish: () => void } interface FishSlice { fishes: number addFish: () => void } interface SharedSlice { addBearAndFish: () => void reset: () => void } type StoreState = BearSlice & FishSlice & SharedSlice // Create individual slices const createBearSlice: StateCreator = (set) => ({ bears: 0, addBear: () => set((state) => ({ bears: state.bears + 1 })), eatFish: () => set((state) => ({ fishes: Math.max(0, state.fishes - 1) })), }) const createFishSlice: StateCreator = (set) => ({ fishes: 0, addFish: () => set((state) => ({ fishes: state.fishes + 1 })), }) const createSharedSlice: StateCreator = ( set, get ) => ({ addBearAndFish: () => { get().addBear() get().addFish() }, reset: () => set({ bears: 0, fishes: 0 }), }) // Combine slices into a single store with middleware const useBoundStore = create()( devtools( persist( (...a) => ({ ...createBearSlice(...a), ...createFishSlice(...a), ...createSharedSlice(...a), }), { name: 'bound-store' } ) ) ) // Usage function App() { const bears = useBoundStore((state) => state.bears) const fishes = useBoundStore((state) => state.fishes) const addBearAndFish = useBoundStore((state) => state.addBearAndFish) return (

Bears: {bears}

Fishes: {fishes}

) } ``` ### Async Actions Zustand handles async operations naturally. Simply call `set` when your async operation completes - no special middleware required. ```typescript import { create } from 'zustand' interface DataState { data: any[] loading: boolean error: string | null fetchData: () => Promise fetchWithRetry: (retries: number) => Promise } const useDataStore = create()((set, get) => ({ data: [], loading: false, error: null, fetchData: async () => { set({ loading: true, error: null }) try { const response = await fetch('https://api.example.com/data') if (!response.ok) throw new Error('Failed to fetch') const data = await response.json() set({ data, loading: false }) } catch (error) { set({ error: (error as Error).message, loading: false }) } }, fetchWithRetry: async (retries: number) => { for (let i = 0; i < retries; i++) { try { await get().fetchData() if (!get().error) return } catch { if (i === retries - 1) throw new Error('All retries failed') } await new Promise((r) => setTimeout(r, 1000 * (i + 1))) } }, })) // Usage function DataComponent() { const { data, loading, error, fetchData } = useDataStore() if (loading) return
Loading...
if (error) return
Error: {error}
return (
    {data.map((item, i) => (
  • {JSON.stringify(item)}
  • ))}
) } ``` ### Transient Updates - High-Frequency State Changes For frequently changing state (like animations or mouse tracking), use subscriptions with refs to avoid triggering React re-renders on every update. ```typescript import { useEffect, useRef } from 'react' import { create } from 'zustand' interface MouseState { x: number y: number setPosition: (x: number, y: number) => void } const useMouseStore = create()((set) => ({ x: 0, y: 0, setPosition: (x, y) => set({ x, y }), })) function HighFrequencyTracker() { const dotRef = useRef(null) useEffect(() => { // Subscribe directly to state changes without causing re-renders const unsubscribe = useMouseStore.subscribe((state) => { if (dotRef.current) { dotRef.current.style.transform = `translate(${state.x}px, ${state.y}px)` } }) const handleMouseMove = (e: MouseEvent) => { useMouseStore.getState().setPosition(e.clientX, e.clientY) } window.addEventListener('mousemove', handleMouseMove) return () => { unsubscribe() window.removeEventListener('mousemove', handleMouseMove) } }, []) return (
) } ``` ## Summary Zustand excels as a lightweight yet powerful state management solution for React applications. Its primary use cases include managing global application state without provider wrappers, handling complex nested state with Immer integration, persisting user preferences and session data to storage, debugging state changes with Redux DevTools, and sharing state between React and non-React code. The hook-based API makes it intuitive for React developers, while the vanilla store option provides flexibility for framework-agnostic scenarios. Integration patterns typically involve creating a central store with `create()`, using selectors to access specific state slices, leveraging middleware composition for persistence and debugging, and organizing large stores using the slices pattern. Zustand's unopinionated design allows it to adapt to various architectural approaches - from simple single-store setups to complex multi-slice configurations with scoped providers. The library's small bundle size and minimal boilerplate make it an excellent choice for projects of any scale, from small prototypes to large production applications.