メインコンテンツへスキップ
非公式ベータ版翻訳

このページは PageTurner AI で翻訳されました(ベータ版)。プロジェクト公式の承認はありません。 エラーを見つけましたか? 問題を報告 →

combineReducers を超えて

Redux に含まれる combineReducers ユーティリティは非常に便利ですが、意図的に単一の一般的なユースケースに限定されています。具体的には、プレーンな JavaScript オブジェクトであるステートツリーを更新するために、各ステートのスライスを更新する作業を特定のスライスリデューサーに委任します。Immutable.js の Map で構成されるステートツリー、ステートツリーの他の部分をスライスリデューサーへの追加引数として渡すこと、スライスリデューサーの呼び出し順序を制御することなど、他のユースケースは扱いません。また、各スライスリデューサーがどのように動作するかについても関知しません。

そこでよくある疑問は、「これらの他のユースケースを扱うために combineReducers をどう使えばよいか?」です。その答えは単純です。「使わないでください。おそらく別の方法が必要です」。combineReducers のコアユースケースを超えた場合、より「カスタム」なリデューサーロジックを使う時が来たということです。それは一度限りのユースケースのための特定のロジックでも、広く共有可能な再利用関数でも構いません。ここでは典型的なユースケースに対処するためのいくつかの提案をしますが、独自のアプローチを自由に考案してください。

スライスリデューサー間でのデータ共有

同様に、特定のアクションを処理するために sliceReducerAsliceReducerB のステートスライスからデータを必要とする場合や、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 で生成されたリデューサーを使って各スライスリデューサーが独立して更新できる「単純な」ケースを処理しつつ、データをスライス間で共有する必要がある「特別な」ケースを別のリデューサーで処理する方法です。その後、ラッピング関数がこれら2つのリデューサーを順番に呼び出して最終結果を生成します:

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 に組み込まれている唯一の reducer ユーティリティ関数ですが、再利用可能なさまざまなサードパーティ製 reducer ユーティリティが公開されています。Redux アドオンカタログ には、利用可能なサードパーティ製ユーティリティの多くがリストアップされています。あるいは、公開されているユーティリティのいずれもあなたのユースケースを解決しない場合は、まさに必要なことを行う関数を自分自身で作成することも常に可能です。