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

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

目次

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

イミュータビリティはアプリのパフォーマンスを向上させ、プログラミングとデバッグをよりシンプルにします。変更されないデータは、アプリ全体で任意に変更できるデータよりも推論しやすいためです。

特に、Webアプリのコンテキストにおけるイミュータビリティは、高度な変更検出技術をシンプルかつ安価に実装することを可能にし、DOMを更新する計算コストのかかるプロセスを、絶対に必要な場合にのみ発生させるようにします(他のライブラリに対するReactのパフォーマンス改善の基礎)。

詳細情報

記事

Reduxでイミュータビリティが必須なのはなぜですか?

  • Reduxと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と状態が与えられた場合、同じ結果を生成すると仮定します。

ラップされたコンポーネントが純粋であると仮定することで、ルート状態オブジェクトまたはmapStateToPropsから返された値が変更されたかどうかを確認するだけで済みます。変更されていない場合、ラップされたコンポーネントは再レンダリングする必要はありません。

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

次に、ルート状態オブジェクトへの参照と渡された状態オブジェクトに対して浅い等価性チェックを実行し、propsオブジェクトの値への各参照と、mapStateToProps関数を再度実行することから返された値に対して別々の浅いチェックをシリーズで実行します。

詳細情報

ドキュメント

記事

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関数が呼び出されるたびに、ルート状態オブジェクトへの保存された参照と、ストアから渡された現在のルート状態オブジェクトに対して浅い等価性チェックが実行されます。チェックに合格した場合、ルート状態オブジェクトは更新されていないため、コンポーネントを再レンダリングする必要はなく、mapStateToPropsを呼び出す必要もありません。

ただし、チェックに失敗した場合、ルート状態オブジェクトは更新されているため、connectは、ラップされたコンポーネントのpropsが更新されたかどうかを確認するためにmapStateToPropsを呼び出します。

これは、オブジェクト内の各値を個別に浅い等価性チェックを実行し、それらのチェックのいずれかが失敗した場合にのみ再レンダリングをトリガーします。

以下の例では、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などの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でのイミュータビリティは、どのようにコンポーネントを不必要にレンダリングさせる可能性がありますか?

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

このような操作が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

逆に、propsオブジェクトの値が可変オブジェクトを参照している場合、コンポーネントがレンダリングされるべきときにレンダリングされない可能性があることに注意してください

詳細情報

記事

データのイミュータビリティを処理するためのアプローチにはどのようなものがありますか?Immerを使用する必要がありますか?

ReduxでImmerを使用する必要はありません。プレーンなJavaScriptは、正しく記述されていれば、イミュータブルに特化したライブラリを使用しなくても、イミュータビリティを完全に提供できます。

ただし、JavaScriptでイミュータビリティを保証するのは難しく、誤ってオブジェクトを変更してしまい、場所を特定するのが非常に難しいバグを引き起こす可能性があります。このため、Immerなどのイミュータブルな更新ユーティリティライブラリを使用すると、アプリの信頼性が大幅に向上し、アプリの開発がはるかに簡単になる可能性があります。

詳細情報

ディスカッション

イミュータブルな操作にプレーンなJavaScriptを使用することの問題点は何ですか?

JavaScriptは、保証されたイミュータブルな操作を提供するように設計されたものではありません。したがって、Reduxアプリでイミュータブルな操作に使用することを選択した場合、注意する必要のあるいくつかの問題があります。

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

JavaScriptでは、Redux状態ツリーなどのオブジェクトを、それに気付かずに誤って変更してしまう可能性があります。たとえば、深くネストされたプロパティを更新したり、新しいオブジェクトの代わりにオブジェクトへの新しい参照を作成したり、ディープコピーではなく浅いコピーを実行したりすると、すべて意図しないオブジェクトの変更につながる可能性があり、最も経験豊富なJavaScriptコーダーでもつまずく可能性があります。

これらの問題を回避するには、推奨されるイミュータブルな更新パターンに従ってください。

冗長なコード

複雑なネストされた状態ツリーを更新すると、記述が面倒でデバッグが難しい冗長なコードになる可能性があります。

パフォーマンスの低下

JavaScriptのオブジェクトと配列をイミュータブルな方法で操作すると、特に状態ツリーが大きくなるにつれて、遅くなる可能性があります。

イミュータブルなオブジェクトを変更するには、そのコピーを変更する必要があり、すべてのプロパティをコピーする必要があるため、大きなオブジェクトをコピーするのは遅くなる可能性があることを覚えておいてください。

対照的に、Immerのようなイミュータブルライブラリは構造共有を利用でき、コピー元の既存オブジェクトの大部分を再利用した新しいオブジェクトを効率的に返します。

詳細情報

ドキュメント

記事