初期状態の初期化
アプリケーションの状態を初期化するには、主に2つの方法があります。createStore
メソッドは、2番目の引数としてオプションのpreloadedState
値を受け入れることができます。リデューサーは、undefined
である着信状態引数を探し、デフォルトとして使用したい値を返すことで、初期値を指定することもできます。これは、リデューサー内での明示的なチェック、またはデフォルト引数値構文 function myReducer(state = someDefaultValue, action)
を使用して行うことができます。
これら2つのアプローチがどのように相互作用するかは、必ずしも明確ではありません。幸いなことに、プロセスはいくつかの予測可能なルールに従います。各要素がどのように組み合わされるかを示します。
概要
combineReducers()
または同様の手動コードがない場合、preloadedState
は常にリデューサー内の state = ...
よりも優先されます。なぜなら、リデューサーに渡される state
は preloadedState
であり、undefined
ではないため、引数構文は適用されないからです。
combineReducers()
を使用すると、動作はよりニュアンスがあります。preloadedState
で状態が指定されているリデューサーは、その状態を受け取ります。他のリデューサーはundefined
を受け取り、そのため、指定した state = ...
デフォルト引数にフォールバックします。
一般的に、preloadedState
はリデューサーによって指定された状態よりも優先されます。これにより、リデューサーはデフォルト引数としてそれらに意味のある初期データを指定できますが、永続ストレージまたはサーバーからストアをハイドレートするときに既存のデータ(全部または一部)をロードすることもできます。
注意:preloadedState
を使用して初期状態が設定されているリデューサーは、undefined
のstate
が渡された場合の処理のためにデフォルト値を引き続き提供する必要があります。すべてのリデューサーは初期化時に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
初期状態はゼロです。なぜでしょうか?それは、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)
}
}
preloadedState
なしでcreateStore
を呼び出すと、state
が{}
に初期化されます。したがって、state.a
とstate.b
は、a
と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' }
次に、preloadedState
をcreateStore()
への引数として指定しました。結合されたリデューサーから返される状態は、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
リデューサーがstate
として'horse'
を受け取り、それを喜んで返しましたが、b
リデューサーはstate
としてundefined
を受け取ったため、デフォルトのstate
(この例では'wat'
)に対する自身のアイデアを返した理由です。これにより、{ a: 'horse', b: 'wat' }
が返されます。
まとめ
まとめると、Reduxの慣例に従い、state
引数としてundefined
で呼び出されたときにリデューサーから初期状態を返す場合(これを実装する最も簡単な方法は、state
のデフォルト引数値を指定することです)、結合されたリデューサーに対して適切な便利な動作が得られます。createStore()
関数に渡すpreloadedState
オブジェクトの対応する値が優先されますが、何も渡さなかった場合、または対応するフィールドが設定されていない場合は、代わりにリデューサーによって指定されたデフォルトのstate
引数が選択されます。このアプローチは、初期化と既存データのハイドレーションの両方を提供し、データが保持されなかった場合に個々のリデューサーが状態をリセットできるようにするため、うまく機能します。もちろん、このパターンを再帰的に適用できます。combineReducers()
を複数のレベルで使用したり、リデューサーを呼び出して状態ツリーの関連部分を与えることでリデューサーを手動で構成したりできます。