超越 combineReducers
Redux 附带的 combineReducers
工具非常有用,但它有意地限制为处理一个常见的用例:通过将更新每个状态切片的工作委托给特定的切片 reducer,来更新一个作为纯 JavaScript 对象的状态树。它不处理其他用例,例如由 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 语句之外的条件逻辑,函数可以组合起来相互包装,函数可以调用其他函数。也许您需要其中一个切片 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 列出了许多可用的第三方实用程序。或者,如果已发布的实用程序都不能解决您的用例,您始终可以自己编写一个函数来完全满足您的需求。