不可变更新模式
在 先决条件#不可变数据管理 中列出的文章提供了许多关于如何执行基本不可变更新操作的良好示例,例如更新对象中的字段或在数组末尾添加项目。但是,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 使不可变更新成为一个简单的函数和普通的 JavaScript 对象
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]] } }
})
它们可以提供一个有用的替代方案,来代替编写手动不可变更新逻辑。
在 Immutable Data#Immutable Update Utilities 部分的 Redux Addons Catalog 中,可以找到许多不可变更新实用程序的列表。
使用 Redux Toolkit 简化不可变更新
我们的 Redux Toolkit 包含一个 createReducer
工具,它在内部使用 Immer。因此,您可以编写看起来像“修改”状态的 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
函数时才有效,该函数将此 reducer 包装在 Immer 的 produce
函数 中。如果此 reducer 在没有 Immer 的情况下使用,它实际上会修改状态!。仅仅通过查看代码,也不能明显地看出此函数实际上是安全的,并且以不可变的方式更新了状态。请确保您完全理解不可变更新的概念。如果您确实使用它,可能有助于在代码中添加一些注释,解释您的 reducer 使用了 Redux Toolkit 和 Immer。
此外,Redux Toolkit 的 createSlice
工具 将根据您提供的 reducer 函数自动生成 action creator 和 action 类型,并在内部具有相同的 Immer 支持的更新功能。