メインコンテンツへスキップ

イミュータブルなデータ

非公式ベータ版翻訳

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

Redux FAQ: イミュータブルなデータ

イミュータビリティの利点は何ですか?

イミュータビリティはアプリのパフォーマンス向上につながり、プログラミングやデバッグをシンプルにします。データが変化しないため、アプリ内で自由に変更可能なデータよりも推論が容易になるからです。

特にWebアプリケーションにおいて、イミュータビリティは高度な変更検知技術をシンプルかつ低コストで実装可能にします。これにより、DOM更新という計算コストの高い処理が本当に必要な場合のみ実行されることが保証され(Reactが他のライブラリより優れたパフォーマンスを発揮する基盤となっています)、

参考情報

記事

Reduxでイミュータビリティが求められる理由は?

参考情報

ドキュメント

ディスカッション

Reduxのシャローイコールチェックでなぜイミュータビリティが必要なのか?

Reduxのシャローイコールチェックでは、接続されたコンポーネントを正しく更新するためにイミュータビリティが必要です。その理由を理解するには、JavaScriptにおけるシャローイコールチェックとディープイコールチェックの違いを把握する必要があります

シャローイコールチェックとディープイコールチェックの違い

シャローイコールチェック(参照等価性)は、2つの異なる_変数_が同じオブジェクトを参照しているかどうかをチェックするだけです。対照的に、ディープイコールチェック(値等価性)は2つのオブジェクトのプロパティ値をすべて再帰的にチェックする必要があります

したがって、シャローイコールチェックはa === bのようにシンプル(かつ高速)ですが、ディープイコールチェックは2つのオブジェクトのプロパティを再帰的に走査し、各ステップでプロパティの値を比較します

Reduxがシャローイコールチェックを採用するのは、このパフォーマンス改善のためです

参考情報

記事

Reduxはどのようにシャローイコールチェックを使用するか?

ReduxはcombineReducers関数内でシャローイコールチェック(浅い比較)を使用し、変更された新しいルートステートオブジェクトのコピーを返すか、変更がなければ現在のルートステートオブジェクトを返します。

参考情報

ドキュメント

combineReducersはどのようにシャローイコールチェックを使用するのか?

Reduxストアの推奨される構造は、ステートオブジェクトをキーによって複数の「スライス」または「ドメイン」に分割し、各データスライスを管理する個別のリデューサー関数を用意することです。

combineReducersは、キー/値ペアのセットで構成されるハッシュテーブルとして定義されたreducers引数を受け取ることで、この構造スタイルを扱いやすくします。各キーはステートスライスの名前を表し、対応する値はそのスライスを処理するリデューサー関数です。

例えば、ステートの形状が{ todos, counter }の場合、combineReducersの呼び出しは次のようになります:

combineReducers({ todos: myTodosReducer, counter: myCounterReducer })

ここで:

  • キーtodoscounterはそれぞれ独立したステートスライスを参照します;

  • myTodosReducermyCounterReducerはリデューサー関数であり、各々が対応するキーで識別されるステートスライスを処理します。

combineReducersはこれらのキー/値ペアを順次処理します。各繰り返し処理で:

  • 各キーが参照する現在のステートスライスへの参照を作成します;

  • 対応するリデューサーを呼び出し、スライスを渡します;

  • リデューサーから返された(変更されている可能性のある)ステートスライスへの参照を作成します。

処理を進めながら、combineReducersは各リデューサーから返されたステートスライスで構成される新しいステートオブジェクトを構築します。この新しいステートオブジェクトは、現在のステートオブジェクトと同一である場合も異なる場合もあります。combineReducersはここでシャローイコールチェックを使用し、ステートが変更されたかどうかを判定します。

具体的には、各処理ステップでcombineReducersは現在のステートスライスとリデューサーから返されたステートスライスに対してシャローイコールチェックを実行します。リデューサーが新しいオブジェクトを返すと、シャローイコールチェックが失敗し、combineReducershasChangedフラグをtrueに設定します。

すべての処理が完了すると、combineReducershasChangedフラグの状態を確認します。trueの場合、新しく構築されたステートオブジェクトが返されます。falseの場合、_現在の_ステートオブジェクトが返されます。

これは特筆すべき点です:すべてのリデューサーが渡されたのと同じstateオブジェクトを返す場合、combineReducers新しく更新されたものではなく、現在のルートステートオブジェクトを返します。

参考情報

ドキュメント

ビデオ

React-Reduxはどのようにシャローイコールチェックを使用するのか?

React-Reduxは、ラップしているコンポーネントが再レンダリングが必要かどうかを判断するためにシャローイコールチェックを使用します。

これを行うため、ラップされたコンポーネントが純粋であると仮定します。つまり、同じpropsとstateが与えられれば同じ結果を生成するという特性です。

ラップされたコンポーネントが純粋であると仮定することで、ルートステートオブジェクトまたはmapStateToPropsから返された値が変更されたかどうかのみをチェックすればよくなります。変更がなければ、ラップされたコンポーネントは再レンダリングを必要としません。

mapStateToProps関数から返されるpropsオブジェクト内の各値への参照を保持することで変更を検出します。

ルートstateオブジェクトへの参照と渡されたstateオブジェクトに対してシャローイコールチェックを実行し、さらにmapStateToProps関数を再実行して返されるpropsオブジェクトの各値に対しても個別のシャローイコールチェックを行います。

参考情報

ドキュメント

記事

なぜReact-ReduxはmapStateToPropから返されるpropsオブジェクト内の各値をシャローイコールチェックするのか?

React-Reduxはpropsオブジェクト自体ではなく、propsオブジェクト内の各値に対してシャローイコールチェックを実行します。

これは、propsオブジェクトが実際にはプロパティ名とその値(または値を取得・生成するセレクター関数)のハッシュであるためです。例:

function mapStateToProps(state) {
return {
todos: state.todos, // prop value
visibleTodos: getVisibleTodos(state) // selector
}
}

export default connect(mapStateToProps)(TodoApp)

したがって、mapStateToPropsの複数回の呼び出しで返されるpropsオブジェクトをシャローイコールチェックすると常に失敗します。なぜなら毎回新しいオブジェクトが返されるからです。

そのためReact-Reduxは、返されるpropsオブジェクト内の各値への参照を個別に保持します。

参考情報

記事

React-Reduxはシャローイコールチェックをどのように使ってコンポーネントの再レンダリング必要性を判断するか?

React-Reduxのconnect関数が呼び出されるたびに、保持しているルートstateオブジェクトへの参照とストアから渡された現在のルートstateオブジェクトに対してシャローイコールチェックを行います。チェックが成功すると、ルートstateは更新されていないため、コンポーネントの再レンダリングやmapStateToPropsの呼び出しは不要です。

チェックが失敗した場合、ルートstateが更新されたことを意味するため、connectmapStateToPropsを呼び出し、ラップされたコンポーネントのpropsが更新されたかどうかを確認します。

これはオブジェクト内の各値に対して個別にシャローイコールチェックを行い、いずれかのチェックが失敗した場合にのみ再レンダリングをトリガーします。

以下の例では、state.todosgetVisibleTodos()の戻り値が連続するconnect呼び出し間で変化しない場合、コンポーネントは再レンダリングされません:

function mapStateToProps(state) {
return {
todos: state.todos, // prop value
visibleTodos: getVisibleTodos(state) // selector
}
}

export default connect(mapStateToProps)(TodoApp)

逆に、次の例(下記)では、todosの値が常に新しいオブジェクトであるため、値が変化するかどうかに関わらずコンポーネントは常に再レンダリングされます:

// AVOID - will always cause a re-render
function mapStateToProps(state) {
return {
// todos always references a newly-created object
todos: {
all: state.todos,
visibleTodos: getVisibleTodos(state)
}
}
}

export default connect(mapStateToProps)(TodoApp)

mapStateToPropsから返される新しい値とReact-Reduxが保持していた前回の値との間でシャローイコールチェックが失敗した場合、コンポーネントの再レンダリングがトリガーされます。

参考情報

記事

ディスカッション

なぜミュータブルオブジェクトではシャローイコールチェックが機能しないのか?

シャローイコールチェックは、関数が渡されたミュータブルオブジェクトを変更したかどうかを検出できません。

これは、同じオブジェクトを参照する2つの変数は、オブジェクトの値が変更されても常に等しいと判定されるためです。以下のケースでは常にtrueが返ります:

function mutateObj(obj) {
obj.key = 'newValue'
return obj
}

const param = { key: 'originalValue' }
const returnVal = mutateObj(param)

param === returnVal
//> true

paramreturnValueのシャローチェックは、両変数が同じオブジェクトを参照しているかだけを確認します。mutateObj()objの変更版を返しても、渡されたオブジェクト自体は同じです。mutateObj内での値の変更は、シャローチェックには全く関係ありません。

参考情報

記事

ミュータブルオブジェクトでのシャローイコールチェックはReduxに問題を引き起こすか?

ミュータブルオブジェクトでのシャローイコールチェック自体はReduxに問題を引き起こしませんが、React-Reduxなどのストア依存ライブラリには問題を引き起こします

具体的には、combineReducersがリデューサーに渡す状態スライスがミュータブルオブジェクトの場合、リデューサーは直接変更して返すことが可能です。

この場合、combineReducersが実行するシャローイコールチェックは常にパスします。状態スライスの値は変更されていても、オブジェクト自体はリデューサーに渡されたものと同じだからです。

その結果、状態が変更されていてもcombineReducershasChangedフラグをセットしません。他のリデューサーが更新された状態スライスを返さない場合、hasChangedフラグはfalseのままとなり、combineReducersは既存のルート状態オブジェクトを返します。

ストアのルート状態値は更新されますが、オブジェクト自体が同じであるため、React-Reduxなどのバインディングライブラリは状態の変更を検知できず、ラップされたコンポーネントの再レンダリングがトリガーされません。

参考情報

ドキュメント

リデューサーが状態を直接変更すると、なぜReact-Reduxはラップコンポーネントを再レンダリングできないのか?

Reduxリデューサーが渡された状態オブジェクトを直接変更して返す場合、ルート状態の値は変わりますがオブジェクト自体は変わりません。

React-Reduxはラップコンポーネントの再レンダリング必要性を判断するためルート状態オブジェクトのシャローチェックを行いますが、状態の変更を検出できず再レンダリングがトリガーされません。

参考情報

ドキュメント

mapStateToPropsに永続オブジェクトを変更して返すセレクターが、なぜReact-Reduxの再レンダリングを妨げるのか?

mapStateToPropsから返されるpropsオブジェクトの値の1つがconnect呼び出しをまたいで存続するオブジェクト(例: ルートステートオブジェクト)でありながら、セレクター関数によって直接変更されて返される場合、React-Reduxはその変更を検出できず、ラップされたコンポーネントの再レンダーをトリガーしません。

これまで見てきたように、セレクター関数が返す可変オブジェクト内の値は変更されている可能性がありますが、オブジェクト自体は変更されていません。浅い比較はオブジェクト自体のみを比較し、その値は比較しないからです。

たとえば、次のmapStateToProps関数は決して再レンダーをトリガーしません:

// State object held in the Redux store
const state = {
user: {
accessCount: 0,
name: 'keith'
}
}

// Selector function
const getUser = state => {
++state.user.accessCount // mutate the state object
return state
}

// mapStateToProps
const mapStateToProps = state => ({
// The object returned from getUser() is always
// the same object, so this wrapped
// component will never re-render, even though it's been
// mutated
userRecord: getUser(state)
})

const a = mapStateToProps(state)
const b = mapStateToProps(state)

a.userRecord === b.userRecord
//> true

逆に、_イミュータブル_オブジェクトが使用されている場合、コンポーネントは再レンダーすべきでないタイミングで再レンダーする可能性があります

参考情報

記事

ディスカッション

イミュータビリティはどのように浅い比較によるオブジェクト変更検出を可能にするのか?

オブジェクトがイミュータブルである場合、関数内で変更が必要な時は常にオブジェクトの_コピー_に対して変更を行う必要があります。

この変更されたコピーは元のオブジェクトとは_別個の_オブジェクトです。そのため返却されると、浅い比較では渡されたオブジェクトとは異なるオブジェクトと識別され、変更が検出されます。

参考情報

記事

リデューサー内のイミュータビリティが不要なレンダリングを引き起こす仕組み

イミュータブルオブジェクトは直接変更できません。代わりにコピーを作成して変更し、元のオブジェクトはそのまま保持する必要があります。

コピーを変更する場合は問題ありませんが、リデューサー内で変更されていないコピーを返すと、ReduxのcombineReducers関数は渡されたステートスライスオブジェクトとは完全に異なるオブジェクトが返されたと判断します。

combineReducersはこの新しいルートステートオブジェクトをストアに返します。新しいオブジェクトの値は現在のルートステートと同じですが、異なるオブジェクトであるためストアが更新され、最終的には接続された全てのコンポーネントが不要に再レンダリングされます。

この問題を防ぐには、リデューサーがステートを変更しない場合、必ず渡されたステートスライスオブジェクトを返す必要があります。

参考情報

記事

mapStateToProps内のイミュータビリティが不要なレンダリングを引き起こす仕組み

配列のfilterのような特定のイミュータブル操作は、値自体が変更されていなくても常に新しいオブジェクトを返します。

このような操作がmapStateToProps内のセレクター関数として使用されると、React-Reduxが返却されたpropsオブジェクトの各値に対して行う浅い比較は、セレクターが毎回新しいオブジェクトを返すため常に失敗します。

そのため、新しいオブジェクトの値が変更されていなくても、ラップされたコンポーネントは常に再レンダリングされます。

例えば以下のコードは常に再レンダリングをトリガーします:

// A JavaScript array's 'filter' method treats the array as immutable,
// and returns a filtered copy of the array.
const getVisibleTodos = todos => todos.filter(t => !t.completed)

const state = {
todos: [
{
text: 'do todo 1',
completed: false
},
{
text: 'do todo 2',
completed: true
}
]
}

const mapStateToProps = state => ({
// getVisibleTodos() always returns a new array, and so the
// 'visibleToDos' prop will always reference a different array,
// causing the wrapped component to re-render, even if the array's
// values haven't changed
visibleToDos: getVisibleTodos(state.todos)
})

const a = mapStateToProps(state)
// Call mapStateToProps(state) again with exactly the same arguments
const b = mapStateToProps(state)

a.visibleToDos
//> { "completed": false, "text": "do todo 1" }

b.visibleToDos
//> { "completed": false, "text": "do todo 1" }

a.visibleToDos === b.visibleToDos
//> false

逆に、プロパティオブジェクトの値がミュータブルなオブジェクトを参照している場合、コンポーネントが再レンダリングされるべきタイミングでされない可能性があることに注意してください。

参考情報

記事

イミュータブルなデータを扱うアプローチは?Immerは必須ですか?

ReduxでImmerを使う必要はありません。正しく記述されたプレーンなJavaScriptでも、不変性を完全に提供できます。

ただしJavaScriptで不変性を保証するのは困難であり、意図せずオブジェクトを変更してアプリにバグを引き起こす可能性があり、その原因究明は非常に困難です。このためImmerのような不変更新ユーティリティライブラリを使用すると、アプリの信頼性を大幅に向上させ、開発を容易にできます。

参考情報

ディスカッション

プレーンJavaScriptで不変操作を行う場合の問題点は?

JavaScriptは不変操作を保証するよう設計されていません。そのためReduxアプリで不変操作に使用する場合、いくつかの問題点を認識する必要があります。

意図しないオブジェクトの変更

JavaScriptでは、深くネストしたプロパティの更新、新しいオブジェクトではなく既存オブジェクトへの参照の作成、ディープコピーではなくシャローコピーの実行などにより、Reduxステートツリーのようなオブジェクトを意図せず変更する可能性があります。これは経験豊富なJavaScript開発者でも陥りやすい問題です。

これらの問題を回避するには、推奨される不変更新パターンに従ってください。

冗長なコード

複雑にネストしたステートツリーを更新すると、記述が面倒でデバッグが困難な冗長なコードになりがちです。

パフォーマンスの問題

JavaScriptのオブジェクトや配列を不変的に操作すると、特にステートツリーが大規模になるにつれて処理速度が低下する可能性があります。

不変オブジェクトを変更するにはコピーを作成する必要があり、大きなオブジェクトをコピーする場合、すべてのプロパティをコピーするため処理が遅くなります。

これに対しImmerのような不変ライブラリは構造的共有を採用でき、コピー元の既存オブジェクトを大幅に再利用する新しいオブジェクトを返します。

参考情報

ドキュメント

記事