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

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

使用 combineReducers

核心概念

Redux 应用中最常见的状态结构是一个包含多个顶层键的普通 JavaScript 对象,每个键对应特定领域的"切片"数据。类似地,为这种状态结构编写 reducer 逻辑的最常见方法是使用"切片 reducer"函数,每个函数具有相同的 (state, action) 签名,各自负责管理对应状态切片的所有更新。多个切片 reducer 可以响应同一个 action,根据需要独立更新自己的切片,最终将更新后的切片组合成新的状态对象。

由于这种模式非常普遍,Redux 提供了 combineReducers 工具函数来实现此行为。这是一个高阶 reducer 的示例:它接收包含多个切片 reducer 函数的对象,并返回一个新的 reducer 函数。

使用 combineReducers 时需要注意几个重要概念:

  • 首要且最重要的是,combineReducers 仅是一个用于简化编写 Redux reducer 最常见场景的工具函数。您_并非必须_在自己的应用中使用它,它也_无法处理所有可能的情况_。完全可以在不使用它的情况下编写 reducer 逻辑,且经常需要为 combineReducer 无法处理的场景编写自定义 reducer 逻辑。(有关示例和建议,请参阅超越 combineReducers

  • 虽然 Redux 本身对状态组织方式没有强制要求,但 combineReducers 强制实施了几条规则以帮助用户避免常见错误。(详见 combineReducers

  • 一个常见问题是:当派发 action 时,Redux 是否会"调用所有 reducer"?由于实际上只有一个根 reducer 函数,默认答案是"不会"。然而,combineReducers 的特定行为_确实_以这种方式工作。为了组装新的状态树,combineReducers 会使用当前状态切片和当前 action 调用每个切片 reducer,使切片 reducer 有机会响应并更新其状态切片(如果需要)。因此,从这个意义上说,使用 combineReducers 确实会"调用所有 reducer",至少会调用它所包装的所有切片 reducer。

  • 您可以在 reducer 结构的任何层级使用它,而不仅限于创建根 reducer。在不同位置使用多个组合 reducer 来共同构成根 reducer 是非常常见的做法。

定义状态结构

有两种方式可以定义 store 状态的初始结构和内容。首先,createStore 函数可以将 preloadedState 作为其第二个参数,这主要用于使用先前持久化的状态(如浏览器的 localStorage)初始化 store。另一种方式是当 state 参数为 undefined 时,根 reducer 返回初始状态值。这两种方法在初始化状态中有更详细的描述,但使用 combineReducers 时还需注意其他事项。

combineReducers 接收一个包含多个切片 reducer 函数的对象,并创建一个输出具有相同键名的对应状态对象的函数。这意味着如果未向 createStore 提供预加载状态,输入对象中切片 reducer 的键名将决定输出状态对象的键名。这些名称之间的关联关系并非总是显而易见,特别是在使用默认模块导出和对象字面量简写等功能时。

以下示例展示了如何通过对象字面量简写结合 combineReducers 来定义状态结构:

// reducers.js
export default theDefaultReducer = (state = 0, action) => state

export const firstNamedReducer = (state = 1, action) => state

export const secondNamedReducer = (state = 2, action) => state

// rootReducer.js
import { combineReducers, createStore } from 'redux'

import theDefaultReducer, {
firstNamedReducer,
secondNamedReducer
} from './reducers'

// Use object literal shorthand syntax to define the object shape
const rootReducer = combineReducers({
theDefaultReducer,
firstNamedReducer,
secondNamedReducer
})

const store = createStore(rootReducer)
console.log(store.getState())
// {theDefaultReducer : 0, firstNamedReducer : 1, secondNamedReducer : 2}

请注意,由于我们使用了对象字面量简写语法,最终状态中的键名与导入的变量名相同。这可能并非总是期望的行为,对于不熟悉现代 JavaScript 语法的人来说常常会造成困惑。

此外,最终生成的键名显得有些奇怪。通常来说,在状态键名中直接包含"reducer"这类单词并不是最佳实践——键名应该简明地反映其持有数据的领域或类型。这意味着我们需要采取两种策略之一:要么在切片reducer对象中显式指定键名来定义输出状态对象的键结构,要么在使用对象字面简写语法时精心重命名导入的切片reducer变量以设置对应键名。

更合理的用法示例如下:

import { combineReducers, createStore } from 'redux'

// Rename the default import to whatever name we want. We can also rename a named import.
import defaultState, {
firstNamedReducer,
secondNamedReducer as secondState
} from './reducers'

const rootReducer = combineReducers({
defaultState, // key name same as the carefully renamed default export
firstState: firstNamedReducer, // specific key name instead of the variable name
secondState // key name same as the carefully renamed named export
})

const reducerInitializedStore = createStore(rootReducer)
console.log(reducerInitializedStore.getState())
// {defaultState : 0, firstState : 1, secondState : 2}

这种状态结构能更准确地反映数据关系,因为我们精心设置了传递给combineReducers的键名配置。