Reusing Reducer Logic: Patterns for creating reusable reducers">Reusing Reducer Logic: Patterns for creating reusable reducers">
跳到主要內容

重複使用 Reducer 邏輯

隨著應用程式的成長,Reducer 邏輯中的常見模式將開始浮現。您可能會發現 Reducer 邏輯的幾個部分對不同類型的資料執行相同類型的作業,並希望透過重複使用相同的共用邏輯來減少重複,以適用於每種類型的資料。或者,您可能希望在 Store 中處理某種類型的資料的「多個執行個體」。然而,Redux Store 的整體結構會帶來一些權衡:它可以輕鬆追蹤應用程式的整體狀態,但可能會讓「鎖定」需要更新特定狀態部分的動作變得更加困難,特別是如果您正在使用 combineReducers 的話。

舉例來說,假設我們想在應用程式中追蹤多個計數器,名稱分別為 A、B 和 C。我們定義初始的 counter reducer,並使用 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 會使用相同的動作呼叫每個區段 reducer,所以傳送 {type : 'INCREMENT'} 實際上會導致全部三個計數器值增加,而並非只增加其中一個。我們需要想辦法包裝 counter 邏輯,以確保只更新我們關心的計數器。

使用高階 reducer 自訂行為

拆分 reducer 邏輯 中所定義,高階 reducer 是一個函式,它將 reducer 函式作為引數,和/或回傳一個新的 reducer 函式作為結果。它也可以視為「reducer 工廠」。combineReducers 是高階 reducer 的一個範例。我們可以使用這個模式,建立我們自己的 reducer 函式的專門版本,每個版本只回應特定的動作。

專門化 reducer 的兩種最常見方式,是使用給定的前綴或後綴產生新的動作常數,或是附加額外的資訊在動作物件中。以下是它們的範例

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
}
}
}

我們現在應該可以使用其中一個來產生我們的專門計數器 reducer,然後傳送會影響我們關心的狀態部分的動作

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}

我們也可以稍微改變這個方法,並建立一個更通用的高階 reducer,它接受給定的 reducer 函式和名稱或識別碼

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')
})

你甚至可以進一步建立一個通用的過濾高階 reducer

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 中擁有智慧型連接元件的多個執行個體,或重複使用分頁或排序等通用功能的邏輯。

除了使用這種方式產生 reducer 之外,你可能也想要使用相同的方法產生動作建立器,並可以使用輔助函式同時產生兩者。請參閱 動作/reducer 產生器reducer 函式庫,以取得動作/reducer 的公用程式。

集合/項目 reducer 模式

此模式允許您擁有多個狀態,並使用共用 reducer 根據動作物件中的附加參數更新每個狀態。

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;
}
}