Skip to main content

超越 combineReducers

¥Beyond combineReducers

Redux 中包含的 combineReducers 实用程序非常有用,但被故意限制为处理单个常见用例:通过将更新每个状态切片的工作委托给特定的切片缩减器来更新作为普通 Javascript 对象的状态树。它不处理其他用例,例如由 Immutable.js Maps 组成的状态树,尝试将状态树的其他部分作为附加参数传递给切片缩减器,或执行 "ordering" 次切片缩减器调用。它也不关心给定的切片 reducer 如何工作。

¥The combineReducers utility included with Redux is very useful, but is deliberately limited to handle a single common use case: updating a state tree that is a plain Javascript object, by delegating the work of updating each slice of state to a specific slice reducer. It does not handle other use cases, such as a state tree made up of Immutable.js Maps, trying to pass other portions of the state tree as an additional argument to a slice reducer, or performing "ordering" of slice reducer calls. It also does not care how a given slice reducer does its work.

那么,常见的问题是“我如何使用 combineReducers 来处理这些其他用例?”。答案很简单:“你不 - 你可能需要使用其他东西”。一旦你了解了 combineReducers 的核心用例,就可以使用更多 "custom" reducer 逻辑,无论是一次性用例的特定逻辑,还是可以广泛共享的可重用函数。以下是处理几个典型用例的一些建议,但请随意提出你自己的方法。

¥The common question, then, is "How can I use combineReducers to handle these other use cases?". The answer to that is simply: "you don't - you probably need to use something else". Once you go past the core use case for combineReducers, it's time to use more "custom" reducer logic, whether it be specific logic for a one-off use case, or a reusable function that could be widely shared. Here's some suggestions for dealing with a couple of these typical use cases, but feel free to come up with your own approaches.

在切片 reducer 之间共享数据

¥Sharing data between slice reducers

类似地,如果 sliceReducerA 恰好需要 sliceReducerB 状态片中的一些数据来处理特定操作,或者 sliceReducerB 恰好需要整个状态作为参数,则 combineReducers 本身不会处理该情况。这可以通过编写一个自定义函数来解决,该函数知道在这些特定情况下将所需的数据作为附加参数传递,例如:

¥Similarly, if sliceReducerA happens to need some data from sliceReducerB's slice of state in order to handle a particular action, or sliceReducerB happens to need the entire state as an argument, combineReducers does not handle that itself. This could be resolved by writing a custom function that knows to pass the needed data as an additional argument in those specific cases, such as:

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 函数或类似方法可以轻松完成此操作:

¥Another alternative to the "shared-slice updates" issue would be to simply put more data into the action. This is easily accomplished using thunk functions or a similar approach, per this example:

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

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

由于 B 的切片中的数据已在操作中,因此父 reducer 无需执行任何特殊操作即可使该数据可供 sliceReducerA 使用。

¥Because the data from B's slice is already in the action, the parent reducer doesn't have to do anything special to make that data available to sliceReducerA.

第三种方法是使用 combineReducers 生成的 reducer 来处理 "simple" 情况,其中每个切片 reducer 可以独立更新自身,但也使用另一个 reducer 来处理需要跨切片共享数据的 "special" 情况。然后,封装函数可以依次调用这两个 reducer 来生成最终结果:

¥A third approach would be to use the reducer generated by combineReducers to handle the "simple" cases where each slice reducer can update itself independently, but also use another reducer to handle the "special" cases where data needs to be shared across slices. Then, a wrapping function could call both of those reducers in turn to generate the final result:

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:

¥As it turns out, there's a useful utility called reduce-reducers that can make that process easier. It simply takes multiple reducers and runs reduce() on them, passing the intermediate state values to the next reducer in line:

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

请注意,如果你使用 reduceReducers,你应该确保列表中的第一个 reducer 能够定义初始状态,因为后面的 reducer 通常会假设整个状态已经存在并且不会尝试提供默认值。

¥Note that if you use reduceReducers, you should make sure that the first reducer in the list is able to define the initial state, since the later reducers will generally assume that the entire state already exists and not try to provide defaults.

进一步建议

¥Further Suggestions

同样,重要的是要理解 Redux reducer 只是函数。虽然 combineReducers 很有用,但它只是工具箱中的一个工具。函数可以包含除 switch 语句之外的条件逻辑,函数可以组合在一起以相互封装,并且函数可以调用其他函数。也许你需要你的切片 reducer 之一能够重置其状态,并且仅响应总体上的特定操作。你可以这样做:

¥Again, it's important to understand that Redux reducers are just functions. While combineReducers is useful, it's just one tool in the toolbox. Functions can contain conditional logic other than switch statements, functions can be composed to wrap each other, and functions can call other functions. Maybe you need one of your slice reducers to be able to reset its state, and to only respond to specific actions overall. You could do:

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

请注意,combineReducers 不知道也不关心负责管理 a 的 reducer 函数有什么特殊之处。我们不需要修改 combineReducers 来具体知道如何撤消事情 - 我们刚刚将所需的部分构建到一个新的组合函数中。

¥Note that combineReducers doesn't know or care that there's anything special about the reducer function that's responsible for managing a. We didn't need to modify combineReducers to specifically know how to undo things - we just built up the pieces we needed into a new composed function.

此外,虽然 combineReducers 是 Redux 中内置的一个 reducer 实用程序函数,但仍有多种第三方 reducer 实用程序已发布以供重用。Redux 插件目录 列出了许多可用的第三方实用程序。或者,如果已发布的实用程序都不能解决你的用例,你始终可以自己编写一个函数来完全满足你的需要。

¥Also, while combineReducers is the one reducer utility function that's built into Redux, there's a wide variety of third-party reducer utilities that have published for reuse. The Redux Addons Catalog lists many of the third-party utilities that are available. Or, if none of the published utilities solve your use case, you can always write a function yourself that does just exactly what you need.