メインコンテンツへスキップ
非公式ベータ版翻訳

このページは PageTurner AI で翻訳されました(ベータ版)。プロジェクト公式の承認はありません。 エラーを見つけましたか? 問題を報告 →

不変な更新パターン

前提となる概念#不変なデータ管理で紹介されている記事には、オブジェクトのフィールド更新や配列末尾への項目追加など、基本的な不変更新操作の良い例が数多く示されています。ただし、リデューサーではこれらの基本操作を組み合わせてより複雑なタスクを実行する必要がよくあります。ここでは、実装が必要になる可能性のある一般的なタスクの例をいくつか紹介します。

ネストしたオブジェクトの更新

ネストしたデータを更新する際の重要なポイントは、ネストのすべてのレベルを適切にコピーして更新する必要があるということです。これは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
}
}
}
}
}

明らかに、ネストの階層が深くなるほど可読性は低下し、ミスをする可能性も高まります。これが、状態をフラットに保ち、可能な限りリデューサーを合成することが推奨される理由の一つです。

よくある間違い #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などの変更関数を使って変更されます。リデューサーでは状態を直接変更したくないため、これらは通常避けるべきです。そのため、"挿入"や"削除"の動作は次のように書かれることがあります:

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
})

doto-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ユーティリティが含まれています。 これにより、状態を「直接変更」しているように見えるリデューサーを記述できますが、実際には不変的に更新が適用されます。

これにより不変更新ロジックを大幅に簡潔に記述できます。ネストされたデータの例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関数でリデューサーをラップするRedux Toolkitの「マジック」createReducer関数を使用している場合のみです。Immerなしでこのリデューサーを使用すると、実際に状態を変更してしまいます!コードを見ただけでは、この関数が安全に状態を不変更新していることは明らかではありません。不変更新の概念を完全に理解していることを確認してください。使用する場合は、リデューサーがRedux ToolkitとImmerを使用していることを説明するコメントを追加すると良いでしょう。

さらに、Redux ToolkitのcreateSliceユーティリティは、提供されたリデューサー関数に基づいてアクションクリエーターとアクションタイプを自動生成し、内部で同じImmerによる更新機能を持っています。

参考情報