Beyond combineReducers: Examples of reducer logic for other use cases not handled by combineReducers">Beyond combineReducers: Examples of reducer logic for other use cases not handled by combineReducers">
跳至主要內容

超越 combineReducers

Redux 附帶的 combineReducers 實用工具非常有用,但故意限制只處理單一常見使用案例:更新狀態樹,該狀態樹是一個純粹的 JavaScript 物件,透過委派更新每個狀態區塊的工作給特定區塊 reducer 來執行。它處理其他使用案例,例如由 Immutable.js Maps 組成的狀態樹、嘗試將狀態樹的其他部分作為額外參數傳遞給區塊 reducer,或執行區塊 reducer 呼叫的「排序」。它也不關心給定的區塊 reducer 如何執行其工作。

因此,常見的問題是「如何使用 combineReducers 來處理這些其他使用案例?」。答案很簡單:「你不需要使用它 - 你可能需要使用其他東西」。一旦你超越 combineReducers 的核心使用案例,就該使用更多「自訂」reducer 邏輯,無論是針對一次性使用案例的特定邏輯,或可廣泛共用的可重複使用函式。以下是處理其中幾個典型使用案例的一些建議,但歡迎提出你自己的方法。

在區塊 reducer 之間共用資料

類似地,如果 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
}
}

「共用區塊更新」問題的另一種替代方案是簡單地將更多資料放入動作中。這很容易使用 thunk 函式或類似方法來完成,如下例所示

function someSpecialActionCreator() {
return (dispatch, getState) => {
const state = getState()
const dataFromB = selectImportantDataFromB(state)

dispatch({
type: 'SOME_SPECIAL_ACTION',
payload: {
dataFromB
}
})
}
}

由於 B 的區塊中的資料已經在動作中,因此父 reducer 不必執行任何特殊操作即可讓 sliceReducerA 使用該資料。

第三種方法是使用由 combineReducers 產生的 reducer 來處理「簡單」案例,其中每個區塊 reducer 可以獨立更新自身,但還使用另一個 reducer 來處理需要跨區塊共用資料的「特殊」案例。然後,包裝函式可以依序呼叫這兩個 reducer 來產生最終結果

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 的有用實用工具可以簡化這個過程。它只需採用多個 reducer 並對它們執行 reduce(),將中間狀態值傳遞給下一行中的 reducer

// Same as the "manual" rootReducer above
const rootReducer = reduceReducers(combinedReducers, crossSliceReducer)

請注意,如果你使用 reduceReducers,你應該確保清單中的第一個 reducer 能夠定義初始狀態,因為後面的 reducer 通常會假設整個狀態已經存在,而不會嘗試提供預設值。

進一步建議

再次強調,了解 Redux reducer 僅僅 是函式非常重要。雖然 combineReducers 很實用,但它只是工具箱中的其中一個工具。函式可以包含除 switch 陳述式以外的條件式邏輯,函式可以組合以互相包覆,函式也可以呼叫其他函式。也許你需要其中一個 slice reducer 能夠重設其狀態,並且僅對特定動作整體回應。你可以執行

const undoableFilteredSliceA = compose(
undoReducer,
filterReducer('ACTION_1', 'ACTION_2'),
sliceReducerA
)
const rootReducer = combineReducers({
a: undoableFilteredSliceA,
b: normalSliceReducerB
})

請注意,combineReducers 不知道或不關心負責管理 a 的 reducer 函式有什麼特別之處。我們不需要修改 combineReducers 以特別了解如何復原事項 - 我們只需將我們需要的部分建構到新的組合函式中即可。

此外,雖然 combineReducers 是內建於 Redux 中的唯一 reducer 工具函式,但有各種各樣的第三方 reducer 工具已發布供重複使用。Redux Addons Catalog 列出了許多可用的第三方工具。或者,如果沒有任何已發布的工具能解決你的使用案例,你隨時都可以自行撰寫一個函式,執行你需要的確切操作。