跳至主内容
非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

突破 combineReducers 的局限

Redux 内置的 combineReducers 工具非常实用,但它被刻意设计为仅处理单一常见场景:通过将状态更新工作委托给特定切片 reducer,来更新普通 JavaScript 对象构成的状态树。它无法处理其他场景,例如由 Immutable.js Map 构成的状态树、向切片 reducer 传递状态树其他部分作为额外参数,或对切片 reducer 调用进行"排序"。它也不关心具体切片 reducer 的内部实现逻辑。

常见的疑问是:"如何让 combineReducers 处理这些其他场景?"。答案很简单:"你不需要这样做——可能需要改用其他方案"。一旦超出 combineReducers 的核心使用场景,就该采用更'定制化'的 reducer 逻辑,无论是针对特定场景的一次性逻辑,还是可广泛复用的函数。以下是处理几种典型场景的建议,你也可以自由设计自己的解决方案。

在切片 reducers 间共享数据

例如当 sliceReducerA 需要从 sliceReducerB 的状态切片中获取数据来处理特定 action,或 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
}
}

解决"跨切片更新"问题的另一种方案是直接在 action 中携带更多数据。使用 thunk 函数可以轻松实现:

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

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

由于来自 B 切片的数据已存在于 action 中,父级 reducer 无需特殊处理即可让 sliceReducerA 访问这些数据。

第三种方案是:用 combineReducers 生成的 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 能简化此过程。它接收多个 reducers 并对其执行 reduce(),将中间状态值传递给链中的下一个 reducer:

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

注意使用 reduceReducers 时,应确保列表中的第一个 reducer 能定义初始状态,因为后续 reducer 通常会假设整个状态已存在而不会尝试提供默认值。

进阶建议

再次强调,理解 Redux reducers 本质就是普通函数至关重要。虽然 combineReducers 很有用,但它只是工具箱中的一件工具。函数可以包含 switch 语句外的条件逻辑,可以相互组合包装,也可以调用其他函数。例如当你需要某个切片 reducer 能重置状态且仅响应特定 action 时:

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 插件目录列出了许多可用的第三方工具。或者,如果已发布的工具都无法满足您的需求,您也可以自行编写完全符合需求的函数。