Zustand State Management Patterns

Lightweight global state management with Zustand.

Overview#

Zustand is a minimal state management library. This pattern covers:

  • Basic store creation
  • TypeScript integration
  • Persistence middleware
  • Async actions
  • Computed values and selectors
  • DevTools integration

Prerequisites#

npm install zustand

Basic Store#

A simple counter store demonstrating Zustand basics.

1// stores/useCounterStore.ts 2import { create } from 'zustand' 3 4interface CounterStore { 5 count: number 6 increment: () => void 7 decrement: () => void 8 reset: () => void 9} 10 11export const useCounterStore = create<CounterStore>((set) => ({ 12 count: 0, 13 increment: () => set((state) => ({ count: state.count + 1 })), 14 decrement: () => set((state) => ({ count: state.count - 1 })), 15 reset: () => set({ count: 0 }) 16})) 17 18// Usage 19function Counter() { 20 const { count, increment, decrement } = useCounterStore() 21 22 return ( 23 <div> 24 <span>{count}</span> 25 <button onClick={increment}>+</button> 26 <button onClick={decrement}>-</button> 27 </div> 28 ) 29}

Typed Store with Slices#

Combine multiple state slices into a single store.

1// stores/useAppStore.ts 2import { create } from 'zustand' 3 4interface User { 5 id: string 6 name: string 7 email: string 8} 9 10interface UserSlice { 11 user: User | null 12 setUser: (user: User | null) => void 13 logout: () => void 14} 15 16interface UISlice { 17 sidebarOpen: boolean 18 theme: 'light' | 'dark' 19 toggleSidebar: () => void 20 setTheme: (theme: 'light' | 'dark') => void 21} 22 23interface AppStore extends UserSlice, UISlice {} 24 25export const useAppStore = create<AppStore>((set) => ({ 26 // User slice 27 user: null, 28 setUser: (user) => set({ user }), 29 logout: () => set({ user: null }), 30 31 // UI slice 32 sidebarOpen: true, 33 theme: 'light', 34 toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })), 35 setTheme: (theme) => set({ theme }) 36})) 37 38// Select specific values to avoid re-renders 39function Sidebar() { 40 const sidebarOpen = useAppStore((state) => state.sidebarOpen) 41 const toggleSidebar = useAppStore((state) => state.toggleSidebar) 42 43 return sidebarOpen ? <nav>...</nav> : null 44}

Persist Middleware#

Automatically save state to localStorage.

1// stores/useSettingsStore.ts 2import { create } from 'zustand' 3import { persist, createJSONStorage } from 'zustand/middleware' 4 5interface SettingsStore { 6 notifications: boolean 7 language: string 8 setNotifications: (enabled: boolean) => void 9 setLanguage: (lang: string) => void 10} 11 12export const useSettingsStore = create<SettingsStore>()( 13 persist( 14 (set) => ({ 15 notifications: true, 16 language: 'en', 17 setNotifications: (enabled) => set({ notifications: enabled }), 18 setLanguage: (language) => set({ language }) 19 }), 20 { 21 name: 'settings', 22 storage: createJSONStorage(() => localStorage), 23 partialize: (state) => ({ 24 notifications: state.notifications, 25 language: state.language 26 }) 27 } 28 ) 29)

Async Actions#

Handle async operations like API calls.

1// stores/useProductStore.ts 2import { create } from 'zustand' 3 4interface Product { 5 id: string 6 name: string 7 price: number 8} 9 10interface ProductStore { 11 products: Product[] 12 loading: boolean 13 error: string | null 14 fetchProducts: () => Promise<void> 15 addProduct: (product: Omit<Product, 'id'>) => Promise<void> 16 deleteProduct: (id: string) => Promise<void> 17} 18 19export const useProductStore = create<ProductStore>((set, get) => ({ 20 products: [], 21 loading: false, 22 error: null, 23 24 fetchProducts: async () => { 25 set({ loading: true, error: null }) 26 27 try { 28 const response = await fetch('/api/products') 29 if (!response.ok) throw new Error('Failed to fetch') 30 const products = await response.json() 31 set({ products, loading: false }) 32 } catch (error) { 33 set({ error: 'Failed to fetch products', loading: false }) 34 } 35 }, 36 37 addProduct: async (productData) => { 38 try { 39 const response = await fetch('/api/products', { 40 method: 'POST', 41 headers: { 'Content-Type': 'application/json' }, 42 body: JSON.stringify(productData) 43 }) 44 const product = await response.json() 45 set({ products: [...get().products, product] }) 46 } catch (error) { 47 set({ error: 'Failed to add product' }) 48 } 49 }, 50 51 deleteProduct: async (id) => { 52 try { 53 await fetch(`/api/products/${id}`, { method: 'DELETE' }) 54 set({ products: get().products.filter(p => p.id !== id) }) 55 } catch (error) { 56 set({ error: 'Failed to delete product' }) 57 } 58 } 59}))

Cart Store with Computed Values#

Build a shopping cart with computed totals.

1// stores/useCartStore.ts 2import { create } from 'zustand' 3import { persist } from 'zustand/middleware' 4 5interface CartItem { 6 id: string 7 name: string 8 price: number 9 quantity: number 10} 11 12interface CartStore { 13 items: CartItem[] 14 addItem: (item: Omit<CartItem, 'quantity'>) => void 15 removeItem: (id: string) => void 16 updateQuantity: (id: string, quantity: number) => void 17 clearCart: () => void 18} 19 20export const useCartStore = create<CartStore>()( 21 persist( 22 (set) => ({ 23 items: [], 24 25 addItem: (item) => set((state) => { 26 const existing = state.items.find(i => i.id === item.id) 27 28 if (existing) { 29 return { 30 items: state.items.map(i => 31 i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i 32 ) 33 } 34 } 35 36 return { items: [...state.items, { ...item, quantity: 1 }] } 37 }), 38 39 removeItem: (id) => set((state) => ({ 40 items: state.items.filter(i => i.id !== id) 41 })), 42 43 updateQuantity: (id, quantity) => set((state) => ({ 44 items: state.items.map(i => 45 i.id === id ? { ...i, quantity } : i 46 ).filter(i => i.quantity > 0) 47 })), 48 49 clearCart: () => set({ items: [] }) 50 }), 51 { 52 name: 'cart' 53 } 54 ) 55) 56 57// Computed selectors 58export const useCartTotal = () => 59 useCartStore((state) => 60 state.items.reduce((sum, item) => sum + item.price * item.quantity, 0) 61 ) 62 63export const useCartCount = () => 64 useCartStore((state) => 65 state.items.reduce((sum, item) => sum + item.quantity, 0) 66 )

DevTools Integration#

Enable Redux DevTools for debugging.

1// stores/useStore.ts 2import { create } from 'zustand' 3import { devtools } from 'zustand/middleware' 4 5interface Store { 6 // ... store interface 7} 8 9export const useStore = create<Store>()( 10 devtools( 11 (set) => ({ 12 // ... store implementation 13 }), 14 { 15 name: 'MyApp', 16 enabled: process.env.NODE_ENV === 'development' 17 } 18 ) 19)

Server-Side Rendering#

Handle SSR with hydration.

1// stores/useAuthStore.ts 2import { create } from 'zustand' 3import { persist, createJSONStorage } from 'zustand/middleware' 4 5export const useAuthStore = create()( 6 persist( 7 (set) => ({ 8 token: null, 9 setToken: (token) => set({ token }) 10 }), 11 { 12 name: 'auth', 13 storage: createJSONStorage(() => { 14 // Return dummy storage for SSR 15 if (typeof window === 'undefined') { 16 return { 17 getItem: () => null, 18 setItem: () => {}, 19 removeItem: () => {} 20 } 21 } 22 return localStorage 23 }) 24 } 25 ) 26) 27 28// Handle hydration mismatch 29'use client' 30 31import { useEffect, useState } from 'react' 32 33export function useHydratedStore<T>(store: () => T) { 34 const [hydrated, setHydrated] = useState(false) 35 const value = store() 36 37 useEffect(() => { 38 setHydrated(true) 39 }, []) 40 41 return hydrated ? value : null 42}

Best Practices#

  1. Use selectors - Select only the state you need to prevent re-renders
  2. Keep stores focused - Split large stores into smaller, focused ones
  3. Persist wisely - Only persist what's necessary, exclude sensitive data
  4. Use TypeScript - Type your stores for better developer experience
  5. Combine with React Query - Use Zustand for UI state, React Query for server state