不変更新パターン
前提となる概念#不変データの管理に記載されている記事には、オブジェクトのフィールドの更新や配列の末尾へのアイテムの追加など、基本的な更新操作を不変的に実行する方法の優れた例がいくつか示されています。しかし、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つです。
よくある間違い #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:1レベルのみのシャローコピーを作成する
このエラーの別の一般的なバージョンを以下に示します。
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)
}
配列内のアイテムの更新
配列内の1つのアイテムを更新するには、`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**パッケージには、内部的に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
}
})
これは明らかに**はるかに短く、読みやすくなっています。ただし、これは、Immerのproduce
関数でこのReducerをラップするRedux Toolkitの「魔法の」createReducer
関数を使用している場合にのみ正しく機能します。ImmerなしでこのReducerを使用すると、実際には状態が変更されます!**コードを見るだけでは、この関数が安全で状態を不変的に更新していることは明らかではありません。不変更新の概念を完全に理解していることを確認してください。これを使用する場合は、ReducerがRedux ToolkitとImmerを使用していることを説明するコメントをコードに追加すると役立つ場合があります。
さらに、Redux ToolkitのcreateSlice
ユーティリティは、提供するReducer関数に基づいて、アクションクリエイターとアクションタイプを自動的に生成し、同じImmer対応の更新機能を内部で使用します。