Redux 不可变更新模式
在 r 基本概念的不可变数据管理 中给出一些示例,演示了不可变的基本更新操作,例如,更新对象中字段,或者,在数组的末尾数据。然而,reducer 经常需要综合使用这些基本操作去处理更加复杂的任务。下面是一些你可能必须去实现的常见任务的例子。
更新嵌套的对象
更新嵌套数据的关键是必须适当地复制和更新嵌套的每个级别。这往往是那些学习 r 难以理解的概念,当试图更新嵌套对象的时候,有一些具体的问题会经常出现。这些意外的导致了直接变化,应该被避免。
定义新变量不会创建新的实际对象,它只创建另引用到同对象。这个的示例如下:
function updateNestedState(state, action) { let nestedState = state.nestedState // : 这将导致直接已经存在的对象引用-不要这么做! nestedState.nestedField = action.data return { ...state, nestedState } }
这个正确返回了顶层状态对象的浅复制,但是变量 nestedState 依然指向已经存在的对象,这个状态被直接了。
这个的另外常见版本的如下所示:
function updateNestedState(state, action) { // 问题: 这仅仅做了浅复制! let newState = { ...state } // : nestedState 仍然是同对象! newState.nestedState.nestedField = action.data return newState }
做顶层的浅复制是不够的 - nestedState 对象也应该被复制。
不幸的是,正确地使用不变的更新去深度嵌套状态的过程很容易变得冗长难读。 更新 ate.first.second[someId].fourth 的示例大概如下所示:
function updateVeryNestedField(state, action) { return { ...state, first: { ...state.first, second: { ...state.first.second, [actionmeId]: { ...state.first.second[actionmeId], fourth: actionmeValue } } } } }
显然,每一层嵌套使得阅读更加困难,并给了更多犯错的机会。这是其中原因,鼓励你保持状态扁平,尽可能构建 reducer。
在数组中插入和数据
通常, Javascript 数组中使用变化的来,例如,push , unshift, shift 。因为我们不想在 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) { // 这不是我们关心的项-保持原来的值 return item } // 否则, 这是我们关心的-返回更新的值 return { ...item, ...action.item } }) }
不可变更新工具库
因为编写不可变的更新可能变得乏味,所以有许多工具程序库试图抽象出这个过程。这些库在 API 和上有所不同,但都试图提供一种更短和更简洁的方式来编写这些更新。例如, 使不可变更新成为简单的和纯 JavaScript 对象:
var usetate = [{ name: 'John Doe', address: { city: 'London' } }] var newState = immer.produce(usetate, draftState => { draftState[0].name = 'Jon Doe' draftState[0].address.city = 'Paris' //nested update similar to mutable way })
有些,像 ,使用字符串路径作为命令:
state = dotProp.set(state, `todos.${index}.complete`, true)
其他的,例如 (现在过时的 React 不可变助手的复制),使用嵌套数据和助手:
var collection = [1, 2, { a: [12, 17, 15] }] var newCollection = update(collection, { 2: { a: { $splice: [[1, 1, 13, 14]] } } })
这些可以有效的替代了手写不可变更新逻辑。
许多不可变更新工具的列表可以在 的 部分找到。
使用 R Starter Kit 简化不可变更新
我们的 包中包含在内部使用了 Immer 的。 因此,您可以编写看似“变异”状态的 Reducer,但更新实际上是不可改变的。
这允许以更简单的方式编写不可变更新逻辑。这是嵌套数据示例 可能看起来像使用 createReducer:
import { createReducer } from 'r-starter-kit' const initialState = { first: { second: { id1: { fourth: 'a' }, id2: { fourth: 'b' } } } } const reducer = createReducer(initialState, { UPDATE_ITEM: (state, action) => { state.first.second[actionmeId].fourth = actionmeValue } })
这显然更短,更易读。但是,这仅仅在您使用来自 R Starter Kit 中的 createReducer 将这个 reducer 包装在 Immer 的 中才会生效。 如果这个 reducer 脱离 Immer 使用,它实际上会改变 state。而且仅仅依靠,可能不容易发现这个实际上是安全并且更新是不可改变的。请确保您完全理解不可变更新的概念。当您完全理解这些概念后,可能有助于在你的中一些注释,来说明你的 Reducer 正在使用 R Starter Kit 和 Immer。