Immutable Update Patterns: How to correctly update state immutably, with examples of common mistakes">Immutable Update Patterns: How to correctly update state immutably, with examples of common mistakes">
跳至主要內容

不變更新模式

先備觀念#不變資料管理中列出的文章提供了許多範例,說明如何執行基本更新操作而不變,例如更新物件中的欄位或將項目新增到陣列的結尾。然而,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 陣列的內容會使用變異函式(例如 pushunshiftsplice)來修改。由於我們不希望在 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]] } }
})

它們可以提供一個有用的替代方案,用於撰寫手動不可變更新邏輯。

可以在 Redux 外掛程式目錄不可變資料#不可變更新公用程式函式庫 區段中找到許多不可變更新公用程式函式庫的清單。

使用 Redux Toolkit 簡化不可變更新

我們的 Redux Toolkit 套件包含一個 createReducer 公用程式函式庫,它在內部使用 Immer。因此,您可以撰寫看起來會「變異」狀態的簡化器,但更新實際上是不可變地套用的。

這允許以更簡單的方式撰寫不可變更新邏輯。以下是 巢狀資料範例在使用 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 函式 中。如果這個簡化器在沒有 Immer 的情況下使用,它實際上會變異狀態!。而且從程式碼中也無法明顯看出這個函式實際上是安全的,並且會不可變地更新狀態。請務必完全了解不可變更新的概念。如果您確實使用這個函式,可能會對您的程式碼新增一些註解,說明您的簡化器正在使用 Redux Toolkit 和 Immer。

此外,Redux Toolkit 的 createSlice 公用程式函式庫 將根據您提供的簡化器函式自動產生動作建立器和動作類型,並在其中包含相同的 Immer 驅動更新功能。

進一步資訊