中间件:Immer 简化状态修改
Zustand 与 Immer 库无缝集成,提供了一个 immer 中间件,使状态修改更加直观和简单。Immer 允许你通过直接修改一个"草稿"状态来创建新的不可变状态,而不需要手动编写复杂的不可变更新逻辑。
什么是 Immer 中间件?
Immer 是一个用于简化不可变数据处理的 JavaScript 库。Zustand 的 immer 中间件允许你:
- 直接修改状态:使用熟悉的 mutable(可变)语法修改状态
- 自动生成不可变状态:Immer 会根据你的修改自动创建新的不可变状态
- 减少样板代码:避免手动编写
spread操作符(...)和复杂的状态更新逻辑 - 提高代码可读性:使状态更新逻辑更加直观和易于理解
安装与基本使用
安装依赖
immer 中间件是 Zustand 的核心功能之一,不需要额外安装。
基本用法
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)
}
}),
})
)
)在组件中使用
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 的状态更新
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 的状态更新
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 会根据你的修改自动创建一个新的不可变状态。
// 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 特别适合处理嵌套对象的状态更新:
const useUserStore = create(
immer((set, get) => ({
user: {
id: 1,
name: 'John Doe',
profile: {
email: '[email protected]',
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)结合使用:
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 回调中使用条件语句来控制状态修改:
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 的生产构建版本,以获得更好的性能
最佳实践总结
- 优先使用 Immer 处理复杂状态:对于包含嵌套对象或数组的复杂状态,Immer 可以显著提高代码可读性和可维护性
- 保持状态更新的原子性:每个
set调用应该代表一个完整的状态更新操作 - 避免在 Immer 回调中执行副作用:副作用(如 API 请求)应该在 Immer 回调之外处理
- 结合 TypeScript 使用:Immer 与 TypeScript 配合使用效果极佳,可以提供完整的类型安全
- 与其他中间件结合使用:根据需求,将 Immer 与
persist、devtools等中间件结合使用
常见问题与解决方案
1. 忘记导入 immer 中间件
问题:直接使用 state.todos.push() 等可变语法,但没有导入和使用 immer 中间件
解决方案:确保导入并使用 immer 中间件:
import { immer } from 'zustand/middleware/immer'
const useStore = create(immer((set, get) => ({
// Store 定义
})))2. 在 Immer 回调外修改状态
问题:尝试在 set 回调外直接修改状态
解决方案:所有状态修改都必须在 set 回调内部进行:
// 错误:在 set 回调外修改状态
const todo = useTodoStore.getState().todos[0]
todo.completed = true
// 正确:在 set 回调内修改状态
useTodoStore.setState((state) => {
state.todos[0].completed = true
})3. 期望 Immer 处理异步操作
问题:尝试在异步函数中直接修改状态
解决方案:异步操作应该先获取数据,然后在 set 回调中使用 Immer 修改状态:
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,了解如何使用 Redux DevTools 来调试 Zustand Store。