# 中间件：Immer 简化状态修改

URL: https://caijiao.org/zustand/guide/middleware-immer
Source: docs/zustand/guide/middleware-immer.md
Description: 了解如何使用 Zustand 的 Immer 中间件简化状态修改，包括基本用法、与普通状态更新的对比、嵌套对象修改、与其他中间件结合使用以及性能最佳实践等内容。

[Zustand](../) 与 [Immer](https://immerjs.github.io/immer/) 库无缝集成，提供了一个 `immer` 中间件，使状态修改更加直观和简单。Immer 允许你通过直接修改一个"草稿"状态来创建新的不可变状态，而不需要手动编写复杂的不可变更新逻辑。

## 什么是 Immer 中间件？

Immer 是一个用于简化不可变数据处理的 JavaScript 库。Zustand 的 `immer` 中间件允许你：

- **直接修改状态**：使用熟悉的 mutable（可变）语法修改状态
- **自动生成不可变状态**：Immer 会根据你的修改自动创建新的不可变状态
- **减少样板代码**：避免手动编写 `spread` 操作符（`...`）和复杂的状态更新逻辑
- **提高代码可读性**：使状态更新逻辑更加直观和易于理解

## 安装与基本使用

### 安装依赖

`immer` 中间件是 Zustand 的核心功能之一，不需要额外安装。

### 基本用法

```javascript
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer' // 导入 immer 中间件

// 创建一个带有 immer 中间件的 Store
const useTodoStore = create(
  immer(
    (set, get) => ({
      todos: [
        { id: 1, text: '学习 Zustand', completed: false },
        { id: 2, text: '掌握 Immer 中间件', completed: false },
      ],
      
      // 使用 Immer 修改状态
      addTodo: (text) => set((state) => {
        state.todos.push({ id: Date.now(), text, completed: false })
      }),
      
      toggleTodo: (id) => set((state) => {
        const todo = state.todos.find((todo) => todo.id === id)
        if (todo) {
          todo.completed = !todo.completed
        }
      }),
      
      updateTodoText: (id, text) => set((state) => {
        const todo = state.todos.find((todo) => todo.id === id)
        if (todo) {
          todo.text = text
        }
      }),
      
      removeTodo: (id) => set((state) => {
        const index = state.todos.findIndex((todo) => todo.id === id)
        if (index !== -1) {
          state.todos.splice(index, 1)
        }
      }),
    })
  )
)
```

### 在组件中使用

```jsx
function TodoList() {
  const { todos, addTodo, toggleTodo, removeTodo } = useTodoStore()
  const [newTodoText, setNewTodoText] = useState('')

  const handleSubmit = (e) => {
    e.preventDefault()
    if (newTodoText.trim()) {
      addTodo(newTodoText)
      setNewTodoText('')
    }
  }

  return (
    <div>
      <h1>Todo List</h1>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={newTodoText}
          onChange={(e) => setNewTodoText(e.target.value)}
          placeholder="添加新任务"
        />
        <button type="submit">添加</button>
      </form>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
            />
            <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
              {todo.text}
            </span>
            <button onClick={() => removeTodo(todo.id)}>删除</button>
          </li>
        ))}
      </ul>
    </div>
  )
}
```

## Immer 与普通 Zustand 的对比

让我们看看使用 Immer 中间件与不使用 Immer 中间件的区别：

### 不使用 Immer 的状态更新

```javascript
const useTodoStore = create((set, get) => ({
  todos: [],
  
  // 普通方式：需要手动编写不可变更新逻辑
  addTodo: (text) => set((state) => ({
    todos: [...state.todos, { id: Date.now(), text, completed: false }],
  })),
  
  toggleTodo: (id) => set((state) => ({
    todos: state.todos.map((todo) =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ),
  })),
  
  removeTodo: (id) => set((state) => ({
    todos: state.todos.filter((todo) => todo.id !== id),
  })),
}))
```

### 使用 Immer 的状态更新

```javascript
const useTodoStore = create(
  immer((set, get) => ({
    todos: [],
    
    // Immer 方式：直接修改草稿状态
    addTodo: (text) => set((state) => {
      state.todos.push({ id: Date.now(), text, completed: false })
    }),
    
    toggleTodo: (id) => set((state) => {
      const todo = state.todos.find((todo) => todo.id === id)
      if (todo) {
        todo.completed = !todo.completed
      }
    }),
    
    removeTodo: (id) => set((state) => {
      const index = state.todos.findIndex((todo) => todo.id === id)
      if (index !== -1) {
        state.todos.splice(index, 1)
      }
    }),
  }))
)
```

可以看到，使用 Immer 中间件后，状态更新代码更加简洁和直观，不需要手动编写复杂的不可变更新逻辑。

## Immer 的核心概念：Draft（草稿）

当使用 Immer 中间件时，`set` 函数接收的回调函数会被传递一个"草稿"状态（`draft`），而不是实际的状态。你可以直接修改这个草稿状态，Immer 会根据你的修改自动创建一个新的不可变状态。

```javascript
// state 是一个 draft（草稿）状态，你可以直接修改它
set((state) => {
  state.counter += 1
  state.user.name = 'John'
  state.todos.push({ id: 1, text: 'Learn Immer', completed: false })
})
```

Immer 使用了一种称为"代理"（Proxy）的技术来跟踪对草稿状态的所有修改。当你完成对草稿状态的修改后，Immer 会根据这些修改创建一个全新的不可变状态对象。

## 高级用法

### 嵌套对象修改

Immer 特别适合处理嵌套对象的状态更新：

```javascript
const useUserStore = create(
  immer((set, get) => ({
    user: {
      id: 1,
      name: 'John Doe',
      profile: {
        email: 'john@example.com',
        address: {
          city: 'New York',
          country: 'USA',
        },
      },
    },
    
    // 直接修改嵌套对象
    updateEmail: (email) => set((state) => {
      state.user.profile.email = email
    }),
    
    updateCity: (city) => set((state) => {
      state.user.profile.address.city = city
    }),
  }))
)
```

### 与其他中间件结合使用

Immer 中间件可以与其他 Zustand 中间件（如 `persist`）结合使用：

```javascript
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import { immer } from 'zustand/middleware/immer'

const useTodoStore = create(
  persist(
    immer((set, get) => ({
      todos: [],
      addTodo: (text) => set((state) => {
        state.todos.push({ id: Date.now(), text, completed: false })
      }),
      toggleTodo: (id) => set((state) => {
        const todo = state.todos.find((todo) => todo.id === id)
        if (todo) {
          todo.completed = !todo.completed
        }
      }),
    })),
    {
      name: 'todo-storage',
    }
  )
)
```

### 条件修改

你可以在 Immer 回调中使用条件语句来控制状态修改：

```javascript
const useCounterStore = create(
  immer((set, get) => ({
    count: 0,
    
    increment: () => set((state) => {
      state.count += 1
    }),
    
    incrementIfOdd: () => set((state) => {
      if (state.count % 2 !== 0) {
        state.count += 1
      }
    }),
    
    incrementIfBelow: (max) => set((state) => {
      if (state.count < max) {
        state.count += 1
      }
    }),
  }))
)
```

## Immer 的性能考虑

Immer 虽然提供了便捷的状态修改方式，但也需要注意性能：

### 优势

- **减少不必要的重新渲染**：Immer 会智能地比较草稿状态和原始状态，只在实际发生变化时创建新对象
- **高效的代理实现**：Immer 的代理实现非常高效，对于大多数应用来说性能影响可以忽略不计

### 注意事项

- **避免过度修改**：虽然 Immer 很高效，但过度频繁地修改大型状态对象可能会影响性能
- **合理使用选择器**：结合 Zustand 的选择器功能，只订阅需要的状态部分
- **考虑使用生产构建**：在生产环境中使用 Immer 的生产构建版本，以获得更好的性能

## 最佳实践总结

1. **优先使用 Immer 处理复杂状态**：对于包含嵌套对象或数组的复杂状态，Immer 可以显著提高代码可读性和可维护性
2. **保持状态更新的原子性**：每个 `set` 调用应该代表一个完整的状态更新操作
3. **避免在 Immer 回调中执行副作用**：副作用（如 API 请求）应该在 Immer 回调之外处理
4. **结合 TypeScript 使用**：Immer 与 TypeScript 配合使用效果极佳，可以提供完整的类型安全
5. **与其他中间件结合使用**：根据需求，将 Immer 与 `persist`、`devtools` 等中间件结合使用

## 常见问题与解决方案

### 1. 忘记导入 immer 中间件

**问题**：直接使用 `state.todos.push()` 等可变语法，但没有导入和使用 `immer` 中间件

**解决方案**：确保导入并使用 `immer` 中间件：

```javascript
import { immer } from 'zustand/middleware/immer'

const useStore = create(immer((set, get) => ({
  // Store 定义
})))
```

### 2. 在 Immer 回调外修改状态

**问题**：尝试在 `set` 回调外直接修改状态

**解决方案**：所有状态修改都必须在 `set` 回调内部进行：

```javascript
// 错误：在 set 回调外修改状态
const todo = useTodoStore.getState().todos[0]
todo.completed = true

// 正确：在 set 回调内修改状态
useTodoStore.setState((state) => {
  state.todos[0].completed = true
})
```

### 3. 期望 Immer 处理异步操作

**问题**：尝试在异步函数中直接修改状态

**解决方案**：异步操作应该先获取数据，然后在 `set` 回调中使用 Immer 修改状态：

```javascript
const useTodoStore = create(
  immer((set, get) => ({
    todos: [],
    isLoading: false,
    
    fetchTodos: async () => {
      set((state) => { state.isLoading = true })
      try {
        const response = await fetch('https://api.example.com/todos')
        const todos = await response.json()
        set((state) => {
          state.todos = todos
          state.isLoading = false
        })
      } catch (error) {
        set((state) => { state.isLoading = false })
        console.error('Failed to fetch todos:', error)
      }
    },
  }))
)
```

在接下来的章节中，我们将学习 [调试神器：Redux DevTools](devtools.md)，了解如何使用 Redux DevTools 来调试 Zustand Store。
