本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →
不可变更新模式
前置概念#不可变数据管理中列出的文章提供了许多基础不可变更新操作的良好示例,例如更新对象字段或在数组末尾添加项。然而,reducer 通常需要组合使用这些基础操作来完成更复杂的任务。以下是一些你可能需要实现的常见任务示例。
更新嵌套对象
更新嵌套数据的关键在于必须逐层复制并适当更新嵌套结构中的每一级。这对于 Redux 初学者通常是个难点,尝试更新嵌套对象时经常会出现特定问题。这些问题会导致意外的直接变更,应当避免。
正确方法:复制所有嵌套层级
遗憾的是,对深层嵌套状态正确应用不可变更新的过程很容易变得冗长且难以阅读。以下是一个更新 state.first.second[someId].fourth 的示例:
function updateVeryNestedField(state, action) {
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue
}
}
}
}
}
显然,每增加一层嵌套都会降低可读性并增加出错几率。这正是鼓励你保持状态扁平化并尽可能组合 reducer 的多个原因之一。
常见错误 #1:指向相同对象的新变量
定义新变量不会创建新对象实体——它仅创建指向同一对象的另一个引用。此类错误的示例如下:
function updateNestedState(state, action) {
let nestedState = state.nestedState
// ERROR: this directly modifies the existing object reference - don't do this!
nestedState.nestedField = action.data
return {
...state,
nestedState
}
}
此函数确实正确返回了顶层状态对象的浅拷贝,但由于 nestedState 变量仍指向现有对象,导致状态被直接变更。
常见错误 #2:仅对单层进行浅拷贝
此错误的另一个常见变体如下:
function updateNestedState(state, action) {
// Problem: this only does a shallow copy!
let newState = { ...state }
// ERROR: nestedState is still the same object!
newState.nestedState.nestedField = action.data
return newState
}
仅对顶层进行浅拷贝是不够的——nestedState 对象也必须被复制。
在数组中插入和删除项
通常,JavaScript 数组内容会使用诸如 push、unshift 和 splice 等可变方法进行修改。由于我们不应在 reducer 中直接变更状态,通常应避免使用这些方法。因此,你可能会看到类似这样的"插入"或"删除"实现:
function insertItem(array, action) {
return [
...array.slice(0, action.index),
action.item,
...array.slice(action.index)
]
}
function removeItem(array, action) {
return [...array.slice(0, action.index), ...array.slice(action.index + 1)]
}
但请记住,关键在于不修改原始内存引用。只要先创建副本,我们就可以安全地变更副本。请注意这同样适用于数组和对象,但嵌套值仍需遵循相同的更新规则。
这意味着我们也可以这样实现插入和删除函数:
function insertItem(array, action) {
let newArray = array.slice()
newArray.splice(action.index, 0, action.item)
return newArray
}
function removeItem(array, action) {
let newArray = array.slice()
newArray.splice(action.index, 1)
return newArray
}
删除函数还可以这样实现:
function removeItem(array, action) {
return array.filter((item, index) => index !== action.index)
}
更新数组中的项
更新数组中的单个项可通过 Array.map 实现:为需更新的项返回新值,为其他项返回原值:
function updateObjectInArray(array, action) {
return array.map((item, index) => {
if (index !== action.index) {
// This isn't the item we care about - keep it as-is
return item
}
// Otherwise, this is the one we want - return an updated value
return {
...item,
...action.item
}
})
}
不可变更新工具库
由于编写不可变更新代码可能变得繁琐,出现了许多试图抽象此过程的工具库。这些库的 API 和使用方式各不相同,但都致力于提供更简洁的更新写法。例如,Immer 让不可变更新成为简单函数操作:
var usersState = [{ name: 'John Doe', address: { city: 'London' } }]
var newState = immer.produce(usersState, draftState => {
draftState[0].name = 'Jon Doe'
draftState[0].address.city = 'Paris'
//nested update similar to mutable way
})
而像 dot-prop-immutable 这样的库则采用字符串路径进行命令操作:
state = dotProp.set(state, `todos.${index}.complete`, true)
其他工具如 immutability-helper(已弃用的 React Immutability Helpers 插件分支)则使用嵌套值和辅助函数:
var collection = [1, 2, { a: [12, 17, 15] }]
var newCollection = update(collection, {
2: { a: { $splice: [[1, 1, 13, 14]] } }
})
这些工具为手动编写不可变更新逻辑提供了实用的替代方案。
众多不可变更新工具的列表可在 Redux 插件目录 的 不可变数据#不可变更新工具 章节中找到。
使用 Redux Toolkit 简化不可变更新
我们的 Redux Toolkit 包内置了基于 Immer 的 createReducer 工具。因此您可以编写看似"直接修改"状态的 reducer,但更新过程实际以不可变方式实现。
这大幅简化了不可变更新逻辑的编写。以下是使用 createReducer 实现的 嵌套数据示例:
import { createReducer } from '@reduxjs/toolkit'
const initialState = {
first: {
second: {
id1: { fourth: 'a' },
id2: { fourth: 'b' }
}
}
}
const reducer = createReducer(initialState, {
UPDATE_ITEM: (state, action) => {
state.first.second[action.someId].fourth = action.someValue
}
})
这种方式显然更简洁易读。但需注意:该功能仅在您使用 Redux Toolkit 提供的"魔术" createReducer 函数时生效,该函数通过 Immer 的 produce 方法 包裹 reducer。若脱离 Immer 环境使用此 reducer,将导致实际状态突变! 仅通过代码外观也无法直观判断其安全性。请确保完全理解不可变更新的核心概念。若采用此方式,建议添加注释说明您的 reducer 使用了 Redux Toolkit 和 Immer。
此外,Redux Toolkit 的 createSlice 工具 可根据您提供的 reducer 函数自动生成 action 创建器和 action 类型,内部同样采用 Immer 驱动的更新机制。