使用 Immer 编写 Reducers
Redux Toolkit 的 createReducer
和 createSlice
自动内部使用 Immer,让你使用 "变异" 语法编写更简单的不可变更新逻辑。这有助于简化大多数 reducer 的实现。
因为 Immer 本身是一个抽象层,所以理解 Redux Toolkit 为什么使用 Immer,以及如何正确使用它是很重要的。
不可变性和 Redux
不可变性基础
"可变"意味着"可改变"。如果某物是"不可变的",那么它永远不能被改变。
JavaScript 的对象和数组默认都是可变的。如果我创 建一个对象,我可以改变其字段的内容。如果我创建一个数组,我也可以改变其内容:
const obj = { a: 1, b: 2 }
// 对象外部仍然相同,但内容已经改变
obj.b = 3
const arr = ['a', 'b']
// 同样,我们可以改变这个数组的内容
arr.push('c')
arr[1] = 'd'
这被称为 变异 对象或数组。它在内存中的引用仍然是同一个对象或数组,但现在对象内部的内容已经改变。
为了以不可变的方式更新值,你的代码必须对现有的对象/数组进行 复制,然后修改副本。
我们可以手动使用 JavaScript 的数组 / 对象扩展运算符,以及返回新数组副本的数组方法(而不是变异原始数组)来实现这一点:
const obj = {
a: {
// 为了安全地更新 obj.a.c,我们必须复制每一部分
c: 3,
},
b: 2,
}
const obj2 = {
// 复制 obj
...obj,
// 覆盖 a
a: {
// 复制 obj.a
...obj.a,
// 覆盖 c
c: 42,
},
}
const arr = ['a', 'b']
// 创建一个新的 arr 副本,并在末尾添加 "c"
const arr2 = arr.concat('c')
// 或者,我们可以复制原始数组:
const arr3 = arr.slice()
// 然后变异副本:
arr3.push('c')
有关 JavaScript 中不可变性的工作原理的更多信息,请参阅:
Reducers 和不可变更新
Redux 的主要规则之一是,我们的 reducers 永远 不允许变异原始/当前的状态值!
// ❌ 非法 - 默认情况下,这将变异状态!
state.value = 123
在 Redux 中不允许变异状态的原因有几个:
- 它会导致错误,例如 UI 无法正确更新以显示最新的值
- 它使理解状态如何以及为何被更新变得更困难
- 它使编写测试变得更困难
- 它破坏了正确使用 "时间旅行调试" 的能力
- 它违反了 Redux 的预期精神和使用模式
那么,如果我们不能改变原始值,我们应该如何返回更新的状态呢?
Reducers 只能对原始值进行 复制,然后他们可以变异副本。
// ✅ 这是安全的,因为我们做了一个副本
return {
...state,
value: 123,
}
我们已经看到,我们可以通过手动使用 JavaScript 的数组 / 对象扩展运算符和其他返回原始值副本的函数来编写不可变的更新。
当数据是嵌套的时候,这变得更难。不可变更新的一个关键规则是,你必须对需要更新的 每个 嵌套级别进行复制。
这可能看起来像这样:
function handwrittenReducer(state, action) {
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue,
},
},
},
}
}
然而,如果你认为"手动编写这种方式的不可变更新看起来很难记住和正确做到"... 是的,你是对的!:)
手动编写不可变更新逻辑是困难的,而且 在 reducers 中意外地变异状态是 Redux 用户 最常犯的错误。
使用 Immer 进行不可变更新
Immer 是一个简化编写不可变更新逻辑的库。
Immer 提供了一个名为 produce
的函数,它接受两个参数:你的原始 state
和一个回调函数。回调函数会得到一个 "草稿" 版本的 state,在回调函数内部,可以安全地编写改变草稿值的代码。Immer 跟踪所有尝试改变草稿值的操作,然后使用它们的不可变等价物重放这些改变,以创建一个安全的、不可变更新的结果:
import produce from 'immer'
const baseState = [
{
todo: 'Learn typescript',
done: true,
},
{
todo: 'Try immer',
done: false,
},
]
const nextState = produce(baseState, (draftState) => {
// "改变" 草稿数组
draftState.push({ todo: 'Tweet about it' })
// "改变" 嵌套的 state
draftState[1].done = true
})
console.log(baseState === nextState)
// false - 数组被复制了
console.log(baseState[0] === nextState[0])
// true - 第一个项目没有改变,所以引用相同
console.log(baseState[1] === nextState[1])
// false - 第二个项目被复制并更新了
Redux Toolkit 和 Immer
Redux Toolkit 的 createReducer
API 自动内部使用 Immer。所以,传递给 createReducer
的任何 case reducer 函数内部 "改变" state 都是安全的:
const todosReducer = createReducer([], (builder) => {
builder.addCase('todos/todoAdded', (state, action) => {
// 通过调用 push() "改变" 数组
state.push(action.payload)
})
})
反过来,createSlice
内部使用 createReducer
,所以在那里 "改变" state 也是安全的:
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
todoAdded(state, action) {
state.push(action.payload)
},
},
})