Skip
Arish's avatar

19. Zustand State Management


Zustand

Zustand is a small, fast, and scalable state management solution. It's simpler than Redux but more powerful than Context.

Installation

bash
1npm install zustand

Basic Usage

jsx
1import { create } from 'zustand'
2
3// Create a store
4const useStore = create((set) => ({
5  count: 0,
6  increment: () => set((state) => ({ count: state.count + 1 })),
7  decrement: () => set((state) => ({ count: state.count - 1 })),
8  reset: () => set({ count: 0 })
9}))
10
11// Use in components
12function Counter() {
13  const count = useStore((state) => state.count)
14  const increment = useStore((state) => state.increment)
15  const decrement = useStore((state) => state.decrement)
16  
17  return (
18    <div>
19      <p>Count: {count}</p>
20      <button onClick={increment}>+</button>
21      <button onClick={decrement}>-</button>
22    </div>
23  )
24}

Store with Multiple Slices

jsx
1const useStore = create((set, get) => ({
2  // User state
3  user: null,
4  login: (userData) => set({ user: userData }),
5  logout: () => set({ user: null }),
6  
7  // Theme state
8  theme: 'light',
9  toggleTheme: () => set((state) => ({
10    theme: state.theme === 'light' ? 'dark' : 'light'
11  })),
12  
13  // Cart state
14  items: [],
15  addItem: (item) => set((state) => ({
16    items: [...state.items, item]
17  })),
18  removeItem: (id) => set((state) => ({
19    items: state.items.filter(item => item.id !== id)
20  })),
21  
22  // Computed values using get()
23  totalItems: () => get().items.length,
24  totalPrice: () => get().items.reduce((sum, item) => sum + item.price, 0)
25}))

Selecting State

Only re-render when selected state changes:

jsx
1// ✅ Good - only re-renders when count changes
2function Counter() {
3  const count = useStore((state) => state.count)
4  return <p>{count}</p>
5}
6
7// ❌ Bad - re-renders on any state change
8function Counter() {
9  const state = useStore()
10  return <p>{state.count}</p>
11}
12
13// Multiple selections
14function UserInfo() {
15  const user = useStore((state) => state.user)
16  const theme = useStore((state) => state.theme)
17  
18  return <div className={theme}>{user?.name}</div>
19}
20
21// Shallow comparison for objects
22import { shallow } from 'zustand/shallow'
23
24function Cart() {
25  const { items, totalPrice } = useStore(
26    (state) => ({ items: state.items, totalPrice: state.totalPrice() }),
27    shallow
28  )
29}

Async Actions

jsx
1const useStore = create((set, get) => ({
2  users: [],
3  loading: false,
4  error: null,
5  
6  fetchUsers: async () => {
7    set({ loading: true, error: null })
8    
9    try {
10      const response = await fetch('/api/users')
11      const users = await response.json()
12      set({ users, loading: false })
13    } catch (error) {
14      set({ error: error.message, loading: false })
15    }
16  },
17  
18  createUser: async (userData) => {
19    try {
20      const response = await fetch('/api/users', {
21        method: 'POST',
22        body: JSON.stringify(userData)
23      })
24      const newUser = await response.json()
25      set((state) => ({ users: [...state.users, newUser] }))
26      return newUser
27    } catch (error) {
28      set({ error: error.message })
29      throw error
30    }
31  }
32}))
33
34// Usage
35function UserList() {
36  const { users, loading, error, fetchUsers } = useStore()
37  
38  useEffect(() => {
39    fetchUsers()
40  }, [])
41  
42  if (loading) return <p>Loading...</p>
43  if (error) return <p>Error: {error}</p>
44  
45  return (
46    <ul>
47      {users.map(user => (
48        <li key={user.id}>{user.name}</li>
49      ))}
50    </ul>
51  )
52}

Persist Middleware

Save state to localStorage:

jsx
1import { create } from 'zustand'
2import { persist } from 'zustand/middleware'
3
4const useStore = create(
5  persist(
6    (set) => ({
7      theme: 'light',
8      toggleTheme: () => set((state) => ({
9        theme: state.theme === 'light' ? 'dark' : 'light'
10      }))
11    }),
12    {
13      name: 'app-storage', // localStorage key
14      partialize: (state) => ({ theme: state.theme }) // only persist theme
15    }
16  )
17)

DevTools Middleware

jsx
1import { create } from 'zustand'
2import { devtools } from 'zustand/middleware'
3
4const useStore = create(
5  devtools(
6    (set) => ({
7      count: 0,
8      increment: () => set(
9        (state) => ({ count: state.count + 1 }),
10        false,
11        'increment' // action name for devtools
12      )
13    }),
14    { name: 'MyStore' }
15  )
16)

Combine Multiple Middlewares

jsx
1import { create } from 'zustand'
2import { devtools, persist } from 'zustand/middleware'
3
4const useStore = create(
5  devtools(
6    persist(
7      (set) => ({
8        // state and actions
9      }),
10      { name: 'app-storage' }
11    ),
12    { name: 'MyStore' }
13  )
14)

TypeScript Support

tsx
1interface User {
2  id: string
3  name: string
4  email: string
5}
6
7interface Store {
8  user: User | null
9  login: (user: User) => void
10  logout: () => void
11}
12
13const useStore = create<Store>((set) => ({
14  user: null,
15  login: (user) => set({ user }),
16  logout: () => set({ user: null })
17}))

Complete Example: Todo Store

jsx
1import { create } from 'zustand'
2import { persist } from 'zustand/middleware'
3
4const useTodoStore = create(
5  persist(
6    (set, get) => ({
7      todos: [],
8      filter: 'all',
9      
10      addTodo: (text) => set((state) => ({
11        todos: [...state.todos, {
12          id: Date.now(),
13          text,
14          completed: false,
15          createdAt: new Date()
16        }]
17      })),
18      
19      toggleTodo: (id) => set((state) => ({
20        todos: state.todos.map(todo =>
21          todo.id === id ? { ...todo, completed: !todo.completed } : todo
22        )
23      })),
24      
25      deleteTodo: (id) => set((state) => ({
26        todos: state.todos.filter(todo => todo.id !== id)
27      })),
28      
29      setFilter: (filter) => set({ filter }),
30      
31      clearCompleted: () => set((state) => ({
32        todos: state.todos.filter(todo => !todo.completed)
33      })),
34      
35      // Computed
36      filteredTodos: () => {
37        const { todos, filter } = get()
38        switch (filter) {
39          case 'active':
40            return todos.filter(t => !t.completed)
41          case 'completed':
42            return todos.filter(t => t.completed)
43          default:
44            return todos
45        }
46      },
47      
48      stats: () => {
49        const { todos } = get()
50        return {
51          total: todos.length,
52          completed: todos.filter(t => t.completed).length,
53          active: todos.filter(t => !t.completed).length
54        }
55      }
56    }),
57    { name: 'todo-storage' }
58  )
59)
60
61// Usage
62function TodoApp() {
63  const addTodo = useTodoStore((s) => s.addTodo)
64  const filteredTodos = useTodoStore((s) => s.filteredTodos())
65  const stats = useTodoStore((s) => s.stats())
66  
67  // Component implementation
68}

Zustand is a fantastic choice for React state management!