このページは PageTurner AI で翻訳されました(ベータ版)。プロジェクト公式の承認はありません。 エラーを見つけましたか? 問題を報告 →
ステートの初期化
アプリケーションのステートを初期化するには主に2つの方法があります。createStoreメソッドは第2引数としてオプションのpreloadedState値を受け取れます。また、リデューサーは入力ステート引数がundefinedの場合にデフォルト値として使用したい値を返すことで初期値を指定できます。これはリデューサー内部で明示的にチェックする方法か、デフォルト引数構文function myReducer(state = someDefaultValue, action)で実現できます。
これらのアプローチがどのように相互作用するかは必ずしも明確ではありませんが、プロセスには一定の規則があります。以下にその仕組みを説明します。
概要
combineReducers()や類似の手動コードを使用しない場合、preloadedStateは常にリデューサーのstate = ...を上書きします。リデューサーに渡されるstateはpreloadedStateでありundefinedではないため、デフォルト引数構文が適用されないからです。
combineReducers()使用時は挙動がより複雑になります。preloadedStateで指定されたステートを持つリデューサーはその値を受け取ります。他のリデューサーはundefinedを受け取るため、指定したstate = ...デフォルト引数にフォールバックします。
一般的に、preloadedStateはリデューサーが指定するステートより優先されます。これによりリデューサーは自身にとって適切な初期データをデフォルト引数として指定しつつ、永続ストレージやサーバーからストアをハイドレートする際に既存データ(完全または部分的)を読み込むことが可能になります。
注記: preloadedStateで初期ステートが設定されるリデューサーでも、stateがundefinedの場合を扱うためデフォルト値を提供する必要があります。すべてのリデューサーは初期化時にundefinedを渡されるため、undefinedが与えられた場合は何らかの値を返すように記述すべきです。この値はundefined以外なら何でもよく、preloadedStateの該当箇所をデフォルト値として重複して指定する必要はありません。
詳細解説
単一リデューサーの場合
まず単一リデューサーを使用するケースを考えます。combineReducers()を使用しない場合です。
リデューサーは次のようになります:
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}
このリデューサーでストアを作成します:
import { createStore } from 'redux'
const store = createStore(counter)
console.log(store.getState()) // 0
初期ステートが0になる理由は? createStoreの第2引数がundefinedだったためです。これが初回呼び出しでリデューサーに渡されるstateです。Reduxの初期化時にはステートを埋めるための「ダミー」アクションがディスパッチされます。つまりcounterリデューサーはstateがundefinedの状態で呼び出されます。これがデフォルト引数を「活性化」する条件です。したがって、デフォルトのstate値(state = 0)に基づき、stateは0になり、この状態(0)が返されます。
別のシナリオを考えてみましょう:
import { createStore } from 'redux'
const store = createStore(counter, 42)
console.log(store.getState()) // 42
なぜ0ではなく42になるのでしょうか? createStoreが第2引数として42を指定して呼び出されたためです。この引数はダミーアクションと共にリデューサーに渡されるstateになります。今回はstateがundefinedではなく(42です!)、デフォルト引数構文が効果を発揮しません。stateは42となり、42がリデューサーから返されます。
結合リデューサーの場合
次にcombineReducers()を使用するケースを考えます。2つのリデューサーがあるとします:
function a(state = 'lol', action) {
return state
}
function b(state = 'wat', action) {
return state
}
combineReducers({ a, b })で生成されるリデューサーは次のようになります:
// const combined = combineReducers({ a, b })
function combined(state = {}, action) {
return {
a: a(state.a, action),
b: b(state.b, action)
}
}
createStoreをpreloadedStateなしで呼び出す場合、stateは{}で初期化されます。そのためaとbのリデューサーが呼び出される時点でstate.aとstate.bはundefinedになります。aとbの両リデューサーはstate引数としてundefinedを受け取り、デフォルトのstate値を指定している場合はそれが返されます。これにより、結合されたリデューサーは最初の呼び出しで{ a: 'lol', b: 'wat' }という状態オブジェクトを返します。
import { createStore } from 'redux'
const store = createStore(combined)
console.log(store.getState()) // { a: 'lol', b: 'wat' }
別のシナリオを考えてみましょう:
import { createStore } from 'redux'
const store = createStore(combined, { a: 'horse' })
console.log(store.getState()) // { a: 'horse', b: 'wat' }
ここではcreateStore()の引数としてpreloadedStateを指定しました。結合されたリデューサーから返される状態は、aリデューサー用に指定した初期状態と、bリデューサー自身が選択した'wat'というデフォルト引数が組み合わさっています。
結合されたリデューサーの動作を振り返りましょう:
// const combined = combineReducers({ a, b })
function combined(state = {}, action) {
return {
a: a(state.a, action),
b: b(state.b, action)
}
}
このケースではstateが指定されているため{}にフォールバックしません。aフィールドは'horse'ですがbフィールドは存在しません。これがaリデューサーが'horse'をstateとして受け取り問題なく返す一方、bリデューサーはundefinedをstateとして受け取り、独自のデフォルト state(この場合は'wat')を返す理由です。結果として{ a: 'horse', b: 'wat' }が返されます。
まとめ
結論として、Reduxの慣例に従いリデューサーがstate引数にundefinedを受け取った時に初期状態を返す(最も簡単な実装方法はstateのデフォルト引数値を指定すること)ようにすれば、結合されたリデューサーにとって有用な動作が得られます。createStore()関数に渡すpreloadedStateオブジェクトの対応する値が優先されますが、何も渡さなかった場合や対応するフィールドが設定されていない場合は、リデューサーが指定したデフォルトのstate引数が代わりに選択されます。このアプローチは初期化と既存データのハイドレーションの両方を提供しつつ、データが保持されなかった場合に個々のリデューサーが状態をリセットできるため効果的です。もちろん、combineReducers()を複数階層で使用したり、リデューサーを手動で呼び出して状態ツリーの関連部分を渡すことで、このパターンを再帰的に適用できます。