Using combineReducers: Explanations of how combineReducers works in practice">Using combineReducers: Explanations of how combineReducers works in practice">
跳至主要内容

使用 combineReducers

核心概念

Redux 应用中最常见的 state 形状是一个普通的 Javascript 对象,它在每个顶层键中包含“切片”的领域特定数据。类似地,为该 state 形状编写 reducer 逻辑的最常见方法是使用“切片 reducer”函数,每个函数都具有相同的 (state, action) 签名,并且每个函数负责管理对该特定 state 切片的全部更新。多个切片 reducer 可以响应相同的 action,独立更新它们自己的切片(如果需要),并且更新后的切片被组合到新的 state 对象中。

由于这种模式非常常见,Redux 提供了 combineReducers 实用程序来实现该行为。它是一个高阶 reducer的示例,它接受一个包含切片 reducer 函数的对象,并返回一个新的 reducer 函数。

在使用 combineReducers 时,有一些重要的概念需要注意。

  • 首先,combineReducers 只是一个简化编写 Redux reducer 时最常见用例的实用程序函数。您不需要在自己的应用程序中使用它,它不会处理所有可能的情况。完全可以编写不使用它的 reducer 逻辑,并且通常需要为 combineReducer 未处理的情况编写自定义 reducer 逻辑。(有关示例和建议,请参阅 超越 combineReducers。)
  • 虽然 Redux 本身对 state 的组织方式没有意见,但 combineReducers 强制执行一些规则来帮助用户避免常见的错误。(有关详细信息,请参阅 combineReducers。)
  • 一个经常被问到的问题是 Redux 在分派 action 时是否“调用所有 reducer”。由于实际上只有一个根 reducer 函数,因此默认答案是“否,它不会”。但是,combineReducers 具有确实以这种方式工作的特定行为。为了组装新的 state 树,combineReducers 将使用其当前的 state 切片和当前的 action 调用每个切片 reducer,从而使切片 reducer 有机会响应并在需要时更新其 state 切片。因此,从这个意义上说,使用 combineReducers 确实“调用所有 reducer”,或者至少调用它正在包装的所有切片 reducer。
  • 您可以在 reducer 结构的各个级别使用它,而不仅仅是创建根 reducer。在各种位置拥有多个组合的 reducer 非常常见,这些 reducer 组合在一起以创建根 reducer。

定义 State 形状

定义商店状态的初始形状和内容有两种方法。首先,createStore 函数可以将 preloadedState 作为其第二个参数。这主要用于使用之前保存在其他地方的状态(例如浏览器的 localStorage)初始化商店。另一种方法是,当状态参数为 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}

请注意,由于我们使用了定义对象字面量的简写,因此结果状态中的键名与导入的变量名相同。这可能并不总是期望的行为,并且经常会让那些不熟悉现代 JS 语法的人感到困惑。

此外,结果名称有点奇怪。通常,在状态键名中包含“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 的键名。