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 zustandBasic 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#
- Use selectors - Select only the state you need to prevent re-renders
- Keep stores focused - Split large stores into smaller, focused ones
- Persist wisely - Only persist what's necessary, exclude sensitive data
- Use TypeScript - Type your stores for better developer experience
- Combine with React Query - Use Zustand for UI state, React Query for server state
Related Patterns#
- React Query - Server state management
- Context - React Context patterns
- URL State - URL-based state management