combineReducers
を超えて
Redux に含まれている combineReducers
ユーティリティは非常に便利ですが、単一の一般的なユースケースを処理するように意図的に制限されています。それは、プレーンな JavaScript オブジェクトである状態ツリーを、各状態スライスの更新作業を特定のスライスリデューサーに委任することによって更新することです。これは、Immutable.js のマップで構成された状態ツリー、状態ツリーの他の部分をスライスリデューサーへの追加の引数として渡そうとする、またはスライスリデューサーの呼び出しの「順序付け」を実行するなどの他のユースケースを処理しません。また、特定のスライスリデューサーがどのように作業を行うかについても関与しません。
共通の質問は、「combineReducers
を使用して、これらの他のユースケースをどのように処理できますか?」です。その答えは簡単に言うと、「そうはしません。おそらく何か他のものを使用する必要があります」です。combineReducers
のコアユースケースを超えたら、1回限りのユースケースに固有のロジック、または広く共有できる再利用可能な関数など、より「カスタム」なリデューサーロジックを使用する時です。ここでは、これらの典型的なユースケースのいくつかに対処するための提案をいくつか示しますが、独自のアプローチを自由に考案してください。
スライスリデューサー間でのデータの共有
同様に、sliceReducerA
が特定のアクションを処理するために sliceReducerB
の状態スライスからデータを必要とする場合、または sliceReducerB
が引数として状態全体を必要とする場合、combineReducers
はそれ自体を処理しません。これは、特定のケースで必要なデータを追加の引数として渡すことを認識するカスタム関数を作成することで解決できます。例:
function combinedReducer(state, action) {
switch (action.type) {
case 'A_TYPICAL_ACTION': {
return {
a: sliceReducerA(state.a, action),
b: sliceReducerB(state.b, action)
}
}
case 'SOME_SPECIAL_ACTION': {
return {
// specifically pass state.b as an additional argument
a: sliceReducerA(state.a, action, state.b),
b: sliceReducerB(state.b, action)
}
}
case 'ANOTHER_SPECIAL_ACTION': {
return {
a: sliceReducerA(state.a, action),
// specifically pass the entire state as an additional argument
b: sliceReducerB(state.b, action, state)
}
}
default:
return state
}
}
「共有スライスの更新」の問題に対する別の代替案は、アクションにさらに多くのデータを追加することです。これは、この例のように、サンク関数または同様のアプローチを使用して簡単に実現できます。
function someSpecialActionCreator() {
return (dispatch, getState) => {
const state = getState()
const dataFromB = selectImportantDataFromB(state)
dispatch({
type: 'SOME_SPECIAL_ACTION',
payload: {
dataFromB
}
})
}
}
B のスライスからのデータはすでにアクションに含まれているため、親リデューサーは sliceReducerA
でそのデータを利用できるようにするために特別なことをする必要はありません。
3番目のアプローチは、combineReducers
によって生成されたリデューサーを使用して、各スライスリデューサーが独立して自身を更新できる「単純な」ケースを処理し、スライス間でデータを共有する必要がある「特別な」ケースを処理するために別のリデューサーを使用することです。次に、ラッピング関数がそれらの両方のリデューサーを順番に呼び出して最終結果を生成できます。
const combinedReducer = combineReducers({
a: sliceReducerA,
b: sliceReducerB
})
function crossSliceReducer(state, action) {
switch (action.type) {
case 'SOME_SPECIAL_ACTION': {
return {
// specifically pass state.b as an additional argument
a: handleSpecialCaseForA(state.a, action, state.b),
b: sliceReducerB(state.b, action)
}
}
default:
return state
}
}
function rootReducer(state, action) {
const intermediateState = combinedReducer(state, action)
const finalState = crossSliceReducer(intermediateState, action)
return finalState
}
実際には、reduce-reducers という便利なユーティリティがあり、このプロセスを簡単にできます。これは、複数のリデューサーを受け取り、それらに対して reduce()
を実行するだけで、中間状態の値を順番に次のリデューサーに渡します。
// Same as the "manual" rootReducer above
const rootReducer = reduceReducers(combinedReducers, crossSliceReducer)
reduceReducers
を使用する場合は、リスト内の最初のリデューサーが初期状態を定義できるようにする必要があります。後のリデューサーは通常、状態全体がすでに存在することを前提とし、デフォルトを提供しようとしないためです。
さらなる提案
繰り返しますが、Redux リデューサーは *単なる* 関数であることを理解することが重要です。combineReducers
は便利ですが、ツールボックスの中の単なる1つのツールです。関数には switch ステートメント以外の条件付きロジックを含めることができ、関数は互いにラップするように構成でき、関数は他の関数を呼び出すことができます。スライスリデューサーの1つがその状態をリセットできるようにし、全体として特定のアクションにのみ応答する必要があるかもしれません。あなたは実行できます
const undoableFilteredSliceA = compose(
undoReducer,
filterReducer('ACTION_1', 'ACTION_2'),
sliceReducerA
)
const rootReducer = combineReducers({
a: undoableFilteredSliceA,
b: normalSliceReducerB
})
combineReducers
は、a
の管理を担当するリデューサー関数について特別なことは何も認識していないことに注意してください。combineReducers
を変更して、具体的にアンドゥする方法を知る必要はありませんでした。必要なものを新しい構成関数に組み込んだだけです。
また、combineReducers
は Redux に組み込まれている1つのリデューサーユーティリティ関数ですが、再利用のために公開されているさまざまなサードパーティ製のリデューサーユーティリティがあります。Redux アドオンカタログには、利用可能なサードパーティ製のユーティリティが多数掲載されています。または、公開されているユーティリティのいずれもユースケースを解決しない場合は、必要なことを正確に実行する関数を自分で記述できます。