リデューサーロジックの再利用
アプリケーションが成長するにつれて、リデューサーロジックに共通のパターンが現れ始めます。リデューサーロジックのいくつかの部分が、異なる型のデータに対して同じ種類の作業を行っており、各データ型に対して同じ共通ロジックを再利用することで重複を減らしたいと考えるかもしれません。または、ストアで処理されている特定の型のデータの複数の「インスタンス」を持ちたい場合もあります。ただし、Redux ストアのグローバルな構造にはいくつかのトレードオフがあります。アプリケーションの全体的な状態を追跡するのは簡単ですが、特に combineReducers
を使用している場合は、特定の状態の一部を更新する必要があるアクションを「ターゲット」にするのが難しくなる可能性もあります。
例として、アプリケーションで A、B、C という複数のカウンターを追跡したいとします。初期の counter
リデューサーを定義し、combineReducers
を使用して状態を設定します。
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}
const rootReducer = combineReducers({
counterA: counter,
counterB: counter,
counterC: counter
})
残念ながら、この設定には問題があります。combineReducers
は各スライスリデューサーを同じアクションで呼び出すため、{type : 'INCREMENT'}
をディスパッチすると、実際には 3 つすべてのカウンター値が増加してしまい、1 つだけではありません。関心のあるカウンターだけが更新されるように、counter
ロジックをラップする方法が必要です。
高階リデューサーによる動作のカスタマイズ
リデューサーロジックの分割で定義されているように、高階リデューサーとは、引数としてリデューサー関数を取り、結果として新しいリデューサー関数を返す関数のことです。これは「リデューサーファクトリー」と見なすこともできます。combineReducers
は高階リデューサーの一例です。このパターンを使用して、独自のリデューサー関数の特殊バージョンを作成できます。各バージョンは特定のアクションにのみ応答します。
リデューサーを特殊化する最も一般的な 2 つの方法は、指定されたプレフィックスまたはサフィックスを持つ新しいアクション定数を生成するか、アクションオブジェクト内に追加情報を添付することです。これらがどのようなものかを示します。
function createCounterWithNamedType(counterName = '') {
return function counter(state = 0, action) {
switch (action.type) {
case `INCREMENT_${counterName}`:
return state + 1
case `DECREMENT_${counterName}`:
return state - 1
default:
return state
}
}
}
function createCounterWithNameData(counterName = '') {
return function counter(state = 0, action) {
const { name } = action
if (name !== counterName) return state
switch (action.type) {
case `INCREMENT`:
return state + 1
case `DECREMENT`:
return state - 1
default:
return state
}
}
}
これで、これらを使用して特殊化されたカウンターリデューサーを生成し、関心のある状態の部分に影響を与えるアクションをディスパッチできるようになります。
const rootReducer = combineReducers({
counterA: createCounterWithNamedType('A'),
counterB: createCounterWithNamedType('B'),
counterC: createCounterWithNamedType('C')
})
store.dispatch({ type: 'INCREMENT_B' })
console.log(store.getState())
// {counterA : 0, counterB : 1, counterC : 0}
function incrementCounter(type = 'A') {
return {
type: `INCREMENT_${type}`
}
}
store.dispatch(incrementCounter('C'))
console.log(store.getState())
// {counterA : 0, counterB : 1, counterC : 1}
アプローチを少し変更して、指定されたリデューサー関数と名前または識別子の両方を受け入れる、より一般的な高階リデューサーを作成することもできます。
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}
function createNamedWrapperReducer(reducerFunction, reducerName) {
return (state, action) => {
const { name } = action
const isInitializationCall = state === undefined
if (name !== reducerName && !isInitializationCall) return state
return reducerFunction(state, action)
}
}
const rootReducer = combineReducers({
counterA: createNamedWrapperReducer(counter, 'A'),
counterB: createNamedWrapperReducer(counter, 'B'),
counterC: createNamedWrapperReducer(counter, 'C')
})
さらに、汎用的なフィルタリング高階リデューサーを作成することもできます。
function createFilteredReducer(reducerFunction, reducerPredicate) {
return (state, action) => {
const isInitializationCall = state === undefined;
const shouldRunWrappedReducer = reducerPredicate(action) || isInitializationCall;
return shouldRunWrappedReducer ? reducerFunction(state, action) : state;
}
}
const rootReducer = combineReducers({
// check for suffixed strings
counterA : createFilteredReducer(counter, action => action.type.endsWith('_A')),
// check for extra data in the action
counterB : createFilteredReducer(counter, action => action.name === 'B'),
// respond to all 'INCREMENT' actions, but never 'DECREMENT'
counterC : createFilteredReducer(counter, action => action.type === 'INCREMENT')
};
これらの基本的なパターンを使用すると、UI 内にスマート接続コンポーネントの複数のインスタンスを作成したり、ページネーションや並べ替えなどの汎用的な機能に対して共通のロジックを再利用したりできます。
この方法でリデューサーを生成するだけでなく、同じアプローチを使用してアクションクリエーターを生成することもできます。ヘルパー関数を使用すると、両方を同時に生成できます。アクション/リデューサージェネレーター および リデューサー ライブラリで、アクション/リデューサーユーティリティを確認してください。
コレクション/アイテムリデューサーパターン
このパターンを使用すると、複数の状態を持ち、アクションオブジェクト内の追加パラメーターに基づいて各状態を更新するために共通のリデューサーを使用できます。
function counterReducer(state, action) {
switch(action.type) {
case "INCREMENT" : return state + 1;
case "DECREMENT" : return state - 1;
}
}
function countersArrayReducer(state, action) {
switch(action.type) {
case "INCREMENT":
case "DECREMENT":
return state.map( (counter, index) => {
if(index !== action.index) return counter;
return counterReducer(counter, action);
});
default:
return state;
}
}
function countersMapReducer(state, action) {
switch(action.type) {
case "INCREMENT":
case "DECREMENT":
return {
...state,
state[action.name] : counterReducer(state[action.name], action)
};
default:
return state;
}
}