Redux FAQ: イミュータブルデータ
目次
- イミュータビリティの利点は何ですか?
- Reduxでイミュータビリティが必須なのはなぜですか?
- Reduxの浅い等価性チェックでイミュータビリティが必須なのはなぜですか?
- 浅い等価性チェックと深い等価性チェックの違いは何ですか?
- Reduxはどのように浅い等価性チェックを使用していますか?
combineReducers
はどのように浅い等価性チェックを使用していますか?- React-Reduxはどのように浅い等価性チェックを使用していますか?
- React-Reduxは、コンポーネントの再レンダリングが必要かどうかを判断するためにどのように浅い等価性チェックを使用していますか?
- 浅い等価性チェックがミュータブルなオブジェクトで機能しないのはなぜですか?
- ミュータブルなオブジェクトでの浅い等価性チェックはReduxで問題を引き起こしますか?
- リデューサーが状態をミューテートすると、React-Reduxがラップされたコンポーネントを再レンダリングしないのはなぜですか?
- セレクターがミューテートし、永続的なオブジェクトを
mapStateToProps
に返すと、React-Reduxがラップされたコンポーネントを再レンダリングしないのはなぜですか? - イミュータビリティは、浅いチェックがオブジェクトのミューテーションを検出できるようにするのはなぜですか?
- リデューサーでのイミュータビリティが、コンポーネントを不必要にレンダリングさせるのはなぜですか?
- mapStateToPropsでのイミュータビリティが、コンポーネントを不必要にレンダリングさせるのはなぜですか?
- データのイミュータビリティを処理するためのアプローチにはどのようなものがありますか?Immerを使用する必要がありますか?
- イミュータブルな操作にJavaScriptを使用する場合の問題点は何ですか?
イミュータビリティの利点は何ですか?
イミュータビリティはアプリのパフォーマンスを向上させ、プログラミングとデバッグをよりシンプルにします。変更されないデータは、アプリ全体で任意に変更できるデータよりも推論しやすいためです。
特に、Webアプリのコンテキストにおけるイミュータビリティは、高度な変更検出技術をシンプルかつ安価に実装することを可能にし、DOMを更新する計算コストのかかるプロセスを、絶対に必要な場合にのみ発生させるようにします(他のライブラリに対するReactのパフォーマンス改善の基礎)。
詳細情報
記事
- Immerの紹介
- JavaScriptのイミュータビリティのプレゼンテーション(PDF - 利点についてはスライド12を参照)
- React:パフォーマンスの最適化
- 2015年への道におけるJavaScriptアプリケーションアーキテクチャ
Reduxでイミュータビリティが必須なのはなぜですか?
- ReduxとReact-Reduxの両方で浅い等価性チェックを採用しています。特に
- Reduxの
combineReducers
ユーティリティは、呼び出すリデューサーによって引き起こされる参照の変更を浅くチェックします。 - React-Reduxの
connect
メソッドは、ルート状態への参照変更を浅くチェックするコンポーネントを生成し、mapStateToProps
関数からの戻り値もチェックして、ラップされたコンポーネントが実際に再レンダリングする必要があるかどうかを確認します。このような浅いチェックは、正しく機能するためにイミュータビリティを必要とします。
- 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 })
ここで
- キー
todos
とcounter
はそれぞれ個別の状態スライスを参照しています。 - 値
myTodosReducer
とmyCounterReducer
はリデューサー関数であり、それぞれが対応するキーで識別される状態スライスに対して作用します。
combineReducers
は、これらのキー/値ペアのそれぞれを反復処理します。各反復で、次のようにします。
- 各キーが参照する現在の状態スライスへの参照を作成します。
- 適切なリデューサーを呼び出して、スライスを渡します。
- リデューサーによって返される、おそらくミューテートされた状態スライスへの参照を作成します。
反復処理を続けるにつれて、combineReducers
は各リデューサーから返される状態スライスを使用して新しい状態オブジェクトを構築します。この新しい状態オブジェクトは、現在の状態オブジェクトと同じ場合もあれば、異なる場合もあります。combineReducers
が状態が変更されたかどうかを判断するために浅い等価性チェックを使用するのはここです。
具体的には、反復処理の各段階で、combineReducers
は現在の状態スライスとリデューサーから返された状態スライスに対して浅い等価性チェックを実行します。リデューサーが新しいオブジェクトを返すと、浅い等価性チェックは失敗し、combineReducers
はhasChanged
フラグをtrueに設定します。
反復処理が完了した後、combineReducers
はhasChanged
フラグの状態をチェックします。trueの場合、新しく構築された状態オブジェクトが返されます。falseの場合、現在の状態オブジェクトが返されます。
これは強調する価値があります。リデューサーがすべて、渡された同じstate
オブジェクトを返す場合、combineReducers
は新しく更新されたものではなく、現在のルート状態オブジェクトを返します。
詳細情報
ドキュメント
ビデオ
React-Reduxはどのように浅い等価性チェックを使用していますか?
React-Reduxは、ラップしているコンポーネントを再レンダリングする必要があるかどうかを判断するために浅い等価性チェックを使用します。
これを行うために、ラップされたコンポーネントが純粋であると仮定します。つまり、コンポーネントは同じpropsと状態が与えられた場合、同じ結果を生成すると仮定します。
ラップされたコンポーネントが純粋であると仮定することで、ルート状態オブジェクトまたはmapStateToProps
から返された値が変更されたかどうかを確認するだけで済みます。変更されていない場合、ラップされたコンポーネントは再レンダリングする必要はありません。
ルート状態オブジェクトへの参照と、mapStateToProps
関数から返されるpropsオブジェクト内の各値への参照を保持することで、変更を検出します。
次に、ルート状態オブジェクトへの参照と渡された状態オブジェクトに対して浅い等価性チェックを実行し、propsオブジェクトの値への各参照と、mapStateToProps
関数を再度実行することから返された値に対して別々の浅いチェックをシリーズで実行します。
詳細情報
ドキュメント
記事
- API:React-Reduxのconnect関数と
mapStateToProps
- Redux FAQ:なぜコンポーネントが再レンダリングされないのか、または
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.todos
とgetVisibleTodos()
から返された値が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
param
とreturnValue
の浅いチェックは、両方の変数が同じオブジェクトを参照しているかどうかをチェックするだけです。mutateObj()
はobj
の変更されたバージョンを返す可能性がありますが、渡されたものと同じオブジェクトです。値がmutateObj
内で変更されたという事実は、浅いチェックにはまったく関係ありません。
詳細情報
記事
可変オブジェクトを使用した浅い等価性チェックは、Reduxで問題を引き起こしますか?
可変オブジェクトを使用した浅い等価性チェックはReduxでは問題を引き起こしませんが、React-Reduxなど、ストアに依存するライブラリでは問題を引き起こします。
具体的には、combineReducers
によってリデューサーに渡される状態スライスが可変オブジェクトの場合、リデューサーはそれを直接変更して返すことができます。
そうすると、リデューサーから返された状態スライスの値は変更されている可能性がありますが、オブジェクト自体は変更されていないため(リデューサーに渡された同じオブジェクト)、combineReducers
が実行する浅い等価性チェックは常に合格します。
したがって、状態が変更されていても、combineReducers
はhasChanged
フラグを設定しません。他のリデューサーが新しい、更新された状態スライスを返さない場合、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のようなイミュータブルライブラリは構造共有を利用でき、コピー元の既存オブジェクトの大部分を再利用した新しいオブジェクトを効率的に返します。
詳細情報
ドキュメント
記事