
Redux 核心机制详解State、Actions 与 Reducers 的设计、编写与 combineReducers 原理【免费下载链接】reduxA JS library for predictable global state management项目地址: https://gitcode.com/gh_mirrors/re/redux本文基于 Redux 官方教程 Fundamentals 系列的 Part 3系统讲解 Redux 三大基石——State状态、Actions动作与 Reducers归约函数如何把业务需求转化为纯 JS 数据结构的 state如何设计描述发生了什么的 action 对象如何编写严格遵循纯函数 不可变更新规则的 reducer 函数以及如何把庞大的根 reducer 拆分为按feature组织的 slice 文件再用combineReducers组装回单一根 reducer。文章会对照当前仓库redux5.0.1 的源码src/combineReducers.ts、src/createStore.ts等解释状态初始化、action 校验与未变化则短路等底层机制。读完本文你将能够独立设计 state 结构、写出合规的 reducer 与 action 清单并理解combineReducers在运行时到底做了什么检查。Redux 5.x 的核心包由 src/index.ts 统一导出主要 API 包括combineReducers本文重点、applyMiddleware、bindActionCreators、compose以及已被标记弃用的createStore对应的类型定义位于 src/types/reducers.ts 与 src/types/actions.ts。前置知识与学习项目准备Part 2 已建立的背景Redux 的价值在于为全局应用状态提供一个唯一的中央存放点store。围绕它有两个核心概念Dispatching分发通过dispatch派发 action 对象这是改变状态的唯一途径Reducer 函数接收当前 state 与 action返回新的 state。Part 3 的任务就是把这两个概念落地为可运行的代码。教程配套工程与启动方式官方教程配套了一个预配置的示例工程React 内置样式 一个假 REST API你可以按以下方式使用在 CodeSandbox 中打开并 fork 官方嵌入的redux-fundamentals-example-app工程也可以克隆对应的reduxjs/redux-fundamentals-example-app仓库然后执行npm install安装依赖、npm start启动项目如果只想看最终成品官方提供了tutorial-steps分支可对照学习。完成本教程后官方推荐使用 Redux 的 Create-React-App 模板reduxjs/cra-template-redux创建新项目它已预装 Redux Toolkit 与 React-Redux内置一个现代化改造版的 counter 示例即 Part 1 中看到的那个示例。不借助模板从零搭建时步骤如下安装reduxjs/toolkit和react-redux两个包使用 RTK 的configureStoreAPI 创建 store并传入至少一个 reducer 函数在应用入口文件如src/index.js中导入 Redux store用 React-Redux 的Provider组件包裹根组件root.render( Provider store{store} App / /Provider, document.getElementById(root) )注意版本现状本仓库当前 package.json 中redux的版本为5.0.1。在 5.x 中createStore已被标记为deprecated官方推荐改用 Redux Toolkit 的configureStore详见 src/createStore.ts 中的弃用注释以及 docs/introduction/why-rtk-is-redux-today.md。本教程为了讲清底层原理仍以createStore为主但本文所有 state/action/reducer 设计原则与combineReducers机制在configureStore下完全一致——RTK 的configureStore实际上会自动调用combineReducers。初始工程结构速览教程示例工程基于 Vite 标准模板改造/src目录下的关键文件index.js应用入口渲染主App组件App.js主应用组件index.css全局样式/apiclient.js是一个fetch的轻量封装支持 GET/POSTserver.js提供假的 REST API 端点后续章节会用到/exampleAddons存放教程后面会用来演示的 Redux 附加组件。定义 Todo 示例应用的需求教程用一个经典的 Todo 应用来串联 state/action/reducer 的全部知识点因为 Todo 应用能覆盖真实应用中最常见的几类操作维护一个条目列表、处理用户输入、数据变化时刷新 UI。初始业务需求如下UI 由三个主要区域组成一个输入框让用户输入新 Todo 条目的文本一个展示所有已有 Todo 条目的列表一个底部区域显示未完成的 Todo 数量并提供筛选选项。列表条目应带复选框可切换completed状态还应能为条目从预定义颜色列表中选择一个颜色分类标签以及支持删除条目计数器应根据未完成任务数量做单复数变化0 items、1 item、3 items应有两个按钮把全部 Todo 标记为已完成、清除删除所有已完成的 Todo提供两种筛选方式按 All / Active / Completed 筛选按一个或多个颜色筛选显示标签颜色匹配的 Todo。最终效果如官方截图所示设计 State 值与状态结构React 与 Redux 的一条核心原则是UI 应该基于 state 构建。因此设计应用的一个有效思路是先枚举出描述应用行为所需的全部 state并尽可能用最少的值来描述 UI——state 越少需要维护和更新的数据就越少。从需求中提炼 state概念上这个应用有两块主要 state当前的 Todo 条目列表本身当前的筛选项。另外还需要记录用户在Add Todo输入框里正在输入的内容但这块相对次要教程留到后面再处理。每个 Todo 条目需要存储用户输入的文本表示是否完成的布尔标志一个唯一 ID一个颜色分类如果选择了的话。筛选行为可以用枚举值描述完成状态All、Active、Completed颜色Red、Yellow、Green、Blue、Orange、Purple。从这些值还可以看出Todos 属于app state应用处理的核心数据而筛选值属于UI state描述应用当前正在做什么的状态。区分这两类 state 有助于理解它们各自的用途。根状态结构示例在 Redux 中应用状态永远保存在纯 JavaScript 对象和数组里。这意味着 state 中不能放类实例、Map/Set/Promise/Date这类内置 JS 类型、函数或任何非纯 JS 数据。Redux 的根 state 值几乎总是一个纯 JS 对象其他数据嵌套其中。据此本应用的 state 结构为一个 Todo 条目对象数组每个条目包含id唯一数字text用户输入的文本completed布尔标志color可选的颜色分类筛选选项当前的 completed 筛选值当前选中的颜色分类数组。完整的 state 示例const todoAppState { todos: [ { id: 0, text: Learn React, completed: true }, { id: 1, text: Learn Redux, completed: false, color: purple }, { id: 2, text: Build something fun!, completed: false, color: blue } ], filters: { status: Active, colors: [red, blue] } }需要特别强调在 Redux 之外拥有其他 state 值是完全允许的本例目前足够小所有状态都放在了 Redux store 里但正如后续教程会看到的有些数据并不需要进 Redux例如这个下拉框是否打开、表单输入框的当前值。设计 ActionsAction 是带type字段的纯 JS 对象。可以把一个 action 理解为一个事件描述应用中刚刚发生的事情。就像基于需求设计 state 结构一样也可以列出描述会发生什么的 action 清单基于用户输入的文本添加一个新的 Todo 条目切换某个 Todo 的 completed 状态为某个 Todo 选择颜色分类删除一个 Todo把所有 Todo 标记为已完成清除所有已完成的 Todo更换 completed 筛选值新增一个颜色筛选移除一个颜色筛选。描述发生了什么所需的额外数据通常放在action.payload字段中——它可以是数字、字符串或包含多个字段的对象。Redux store 并不关心action.type的实际字符串是什么但你自己的代码会靠action.type判断是否需要更新调试时你也经常会在 Redux DevTools 扩展里查看这些 type 字符串。所以action type 要选得可读、能清楚描述发生了什么日后排查问题会轻松得多。基于上面的清单本应用使用的 8 个 action 为{type: todos/todoAdded, payload: todoText}{type: todos/todoToggled, payload: todoId}{type: todos/colorSelected, payload: {todoId, color}}{type: todos/todoDeleted, payload: todoId}{type: todos/allCompleted}{type: todos/completedCleared}{type: filters/statusFilterChanged, payload: filterValue}{type: filters/colorFilterChanged, payload: {color, changeType}}这里大部分 action 只有一项额外数据直接放进action.payload即可。颜色筛选本可以拆成新增和移除两个 action但教程故意用一个带额外字段的 action 来表示以演示payload 也可以是对象。和 state 数据一样action 应只包含描述发生了什么所需的最小信息。源码印证action 的最低契约是{ type: T extends string }见 src/types/actions.tsstore 的dispatch在运行时会强制校验action 必须是纯对象、type不能为undefined、type必须是字符串见 src/createStore.ts 中的dispatch实现——上面清单里的 8 个 action 全部满足这些约束。编写 ReducersReducer 是接收当前state和action两个参数、返回新state的函数即(state, action) newState。对应到类型系统src/types/reducers.ts 中的定义是export type Reducer S any, A extends Action UnknownAction, PreloadedState S (state: S | PreloadedState | undefined, action: A) S注意返回类型是S而不是S | undefined——reducer 永远不允许返回undefined这一点在combineReducers的运行时校验中会被反复强调见后文源码剖析。创建根 Reducer一个 Redux 应用实际上只有一个 reducer 函数传入createStore的根 reducerroot reducer。它负责处理所有被派发的 action并计算每次的完整新 state。在src文件夹与index.js、App.js同层创建reducer.js。每个 reducer 都需要初始 state所以先加入几条假数据再写出 reducer 逻辑骨架const initialState { todos: [ { id: 0, text: Learn React, completed: true }, { id: 1, text: Learn Redux, completed: false, color: purple }, { id: 2, text: Build something fun!, completed: false, color: blue } ], filters: { status: All, colors: [] } } // Use the initialState as a default value export default function appReducer(state initialState, action) { // The reducer normally looks at the action type field to decide what happens switch (action.type) { // Do something here based on the different types of actions default: // If this reducer doesnt recognize the action type, or doesnt // care about this specific action, return the existing state unchanged return state } }reducer 可能在应用初始化时被以undefined作为 state 值调用此时必须提供一个初始 state后面的 reducer 代码才有东西可操作。Reducer 通常用默认参数语法提供初始 state(state initialState, action)。处理todos/todoAdded先检查当前 action 的 type 是否匹配目标字符串然后返回一个包含全部 state 字段的新对象——包括那些没有变化的字段function nextTodoId(todos) { const maxId todos.reduce((maxId, todo) Math.max(todo.id, maxId), -1) return maxId 1 } // Use the initialState as a default value export default function appReducer(state initialState, action) { // The reducer normally looks at the action type field to decide what happens switch (action.type) { case todos/todoAdded: { // We need to return a new state object return { // that has all the existing state data ...state, // but has a new array for the todos field todos: [ // with all of the old todos ...state.todos, // and the new todo object { // Use an auto-incrementing numeric ID for this example id: nextTodoId(state.todos), text: action.payload, completed: false } ] } } default: // If this reducer doesnt recognize the action type, or doesnt // care about this specific action, return the existing state unchanged return state } }加一个 Todo 要写这么多代码为什么这就引出了 reducer 必须遵守的规则。Reducer 的三条铁律Reducer 必须始终遵守若干特殊规则只能基于state和action两个参数计算新 state 值不允许修改现有的state必须做不可变更新immutable updates——复制现有state再对复制出来的值做修改不能执行任何异步逻辑或其他副作用。副作用指任何在从函数返回值之外可观察到的状态或行为变化常见的副作用包括向控制台打印日志保存文件设置异步定时器发起 HTTP 请求修改函数外部的某个状态或原地修改mutate函数的参数生成随机数或唯一随机 ID如Math.random()、Date.now()。凡是满足这些规则的函数都叫**纯函数**即使它并不是以 reducer 的形式写出来的。为什么这些规则如此重要原因有几方面Redux 的目标之一是可预测性当函数的输出只由输入参数决定时更容易理解它的行为也更容易测试反之如果函数依赖外部变量或行为随机你就永远不知道运行结果会是什么如果函数修改了其他值包括参数会出乎意料地改变应用行为——这是常见 bug 来源比如我明明更新了 state为什么 UI 该刷新时却不刷新Redux DevTools 的部分能力如时间旅行调试依赖 reducer 正确遵守这些规则。其中不可变更新这条规则尤其重要值得单独展开。Reducer 与不可变更新Redux 中的两个基本概念是mutation变更/原地修改与immutability不可变性把值视为不可更改。在 Redux 中reducer 绝对不允许修改原始/当前的 state 值// ❌ Illegal - by default, this will mutate the state! state.value 123为什么在 Redux 中不能 mutate state会引发 bug例如 UI 无法正确更新到最新值更难理解 state 为什么、如何被更新更难编写测试破坏时间旅行调试的正确运行违背 Redux 的设计初衷与使用模式。既然不能改原始值返回更新后的 state 该怎么做Reducer 只能对原始值做复制然后才可以修改副本。// ✅ This is safe, because we made a copy return { ...state, value: 123 }Part 2 已经介绍过手动写不可变更新的方式使用 JS 的数组/对象展开运算符以及其他返回原值副本的函数如map、concat。当数据是嵌套的时这会变难。不可变更新的一条关键规则是必须对每一个需要更新的嵌套层级都做一次复制。如果你觉得手写这种不可变更新既难记又容易错——没错官方也承认手写确实困难而且在 reducer 中不小心 mutate state 是 Redux 用户犯下的第一大错误。好消息在实际应用中你不需要手写这些复杂的嵌套不可变更新。教程 Part 8Modern Redux with Redux Toolkit见 part-8会讲如何用 Redux Toolkit如createSlice内部基于 Immer简化 reducer 中的不可变更新写法。继续处理更多 Actions在掌握上述规则后继续往根 reducer 中加逻辑。先按 ID 切换某个 Todo 的completed字段export default function appReducer(state initialState, action) { switch (action.type) { case todos/todoAdded: { return { ...state, todos: [ ...state.todos, { id: nextTodoId(state.todos), text: action.payload, completed: false } ] } } case todos/todoToggled: { return { // Again copy the entire state object ...state, // This time, we need to make a copy of the old todos array todos: state.todos.map(todo { // If this isnt the todo item were looking for, leave it alone if (todo.id ! action.payload) { return todo } // Weve found the todo that has to change. Return a copy: return { ...todo, // Flip the completed flag completed: !todo.completed } }) } } default: return state } }再看一个筛选相关的 case处理可见性筛选值变化status 筛选actionexport default function appReducer(state initialState, action) { switch (action.type) { case todos/todoAdded: { return { ...state, todos: [ ...state.todos, { id: nextTodoId(state.todos), text: action.payload, completed: false } ] } } case todos/todoToggled: { return { ...state, todos: state.todos.map(todo { if (todo.id ! action.payload) { return todo } return { ...todo, completed: !todo.completed } }) } } case filters/statusFilterChanged: { return { // Copy the whole state ...state, // Overwrite the filters value filters: { // copy the other filter fields ...state.filters, // And replace the status field with the new value status: action.payload } } } default: return state } }此时只处理了 3 个 action代码已经有些长了。如果继续把每个 action 都塞进这一个 reducer 函数整体会越来越难读——这就是拆分 reducer 的动机。Reducer 通常会被拆分为多个更小的 reducer 函数以便理解和维护 reducer 逻辑。拆分 Reducersslice 文件与 feature 组织Redux reducer 通常按它更新哪一块 state来拆分。本应用的 state 有两个顶层区域state.todos和state.filters因此可以把大的根 reducer 拆成两个小 reducer——todosReducer和filtersReducer。官方建议按feature与某个概念或业务区域相关的代码组织 Redux 应用的文件夹与文件。某个 feature 的 Redux 代码通常写成单个文件即slice 文件其中包含该部分 app state 的全部 reducer 逻辑与所有 action 相关代码。因此管理 state 某一节的 reducer 被称为 slice reducer。通常部分 action 对象与某个 slice reducer 紧密相关其 action type 字符串应以该 feature 名开头如todos并描述发生的事件如todoAdded拼接为一个字符串todos/todoAdded——这正是前文 8 个 action 命名的由来。在工程中创建features文件夹其下建todos文件夹再创建todosSlice.js把 Todo 相关的初始状态剪切粘贴过来const initialState [ { id: 0, text: Learn React, completed: true }, { id: 1, text: Learn Redux, completed: false, color: purple }, { id: 2, text: Build something fun!, completed: false, color: blue } ] function nextTodoId(todos) { const maxId todos.reduce((maxId, todo) Math.max(todo.id, maxId), -1) return maxId 1 } export default function todosReducer(state initialState, action) { switch (action.type) { default: return state } }然后拷贝 Todo 的更新逻辑过来但这里有一个重要区别这个文件只负责更新 todos 相关的 state——它不再嵌套了这也是拆分 reducer 的另一个好处由于 todos state 本身就是一个数组slice 内部无需再复制外层的根 state 对象reducer 因此更易读。这种把多个 reducer 组合起来的方式叫reducer compositionreducer 组合是构建 Redux 应用的基本模式。处理完两个 action 后todosSlice.js变为export default function todosReducer(state initialState, action) { switch (action.type) { case todos/todoAdded: { // Can return just the new todos array - no extra object around it return [ ...state, { id: nextTodoId(state), text: action.payload, completed: false } ] } case todos/todoToggled: { return state.map(todo { if (todo.id ! action.payload) { return todo } return { ...todo, completed: !todo.completed } }) } default: return state } }代码变短了也更好读了。接下来对筛选逻辑做同样的事创建src/features/filters/filtersSlice.js把筛选相关代码移过去const initialState { status: All, colors: [] } export default function filtersReducer(state initialState, action) { switch (action.type) { case filters/statusFilterChanged: { return { // Again, one less level of nesting to copy ...state, status: action.payload } } default: return state } }这里仍然要复制包含 filters state 的对象但由于嵌套层级少了一层逻辑更直白。官方教程为控制篇幅跳过了其余 action如todos/colorSelected、todos/todoDeleted、todos/allCompleted、todos/completedCleared、filters/colorFilterChanged的 reducer 更新逻辑展示建议读者对照前文的需求清单自行练习卡住时可以查阅教程末尾给出的 CodeSandbox 中的完整实现。组合 Reducers手写根 reducer 与 combineReducers现在有了两个独立的 slice 文件、各自拥有一个 slice reducer。但前文说过创建 store 时需要一个根 reducer。如何在不把代码写回一个巨型函数的前提下回到单一根 reducer由于 reducer 就是普通 JS 函数可以把两个 slice reducer 导入reducer.js写一个只负责调用另外两个函数的新根 reducerimport todosReducer from ./features/todos/todosSlice import filtersReducer from ./features/filters/filtersSlice export default function rootReducer(state {}, action) { // always return a new object for the root state return { // the value of state.todos is whatever the todos reducer returns todos: todosReducer(state.todos, action), // For both reducers, we only pass in their slice of the state filters: filtersReducer(state.filters, action) } }注意每个 reducer 各自管理全局 state 的一块。每个 reducer 的state参数都不同对应它自己所管理的那块 state。这就让我们能够按 feature、按 state slice 拆分逻辑保持可维护性。combineReducers观察上面手写的根 reducer它对每个 slice 做的是同一件事——调用 slice reducer、传入该 reducer 所持有的 state 切片、把结果写回根 state 对象。如果再加 slice这个模式会一直重复。Redux 核心库自带一个工具函数combineReducers替我们完成这步样板代码。可以用它生成的更短的根 reducer 替换手写版本。到这一步真正需要安装 Redux 核心库了npm install redux安装后导入并使用combineReducersimport { combineReducers } from redux import todosReducer from ./features/todos/todosSlice import filtersReducer from ./features/filters/filtersSlice const rootReducer combineReducers({ // Define a top-level state field named todos, handled by todosReducer todos: todosReducer, filters: filtersReducer }) export default rootReducercombineReducers接收一个对象键名会成为根 state 对象的键名值是知道如何更新对应 state 切片的 slice reducer 函数。记住你传给combineReducers的键名决定了 state 对象的键名当前仓库自带的示例 examples/todos-with-undo/src/reducers/index.js 就是这一模式的最小化实践import { combineReducers } from redux import todos from ./todos import visibilityFilter from ./visibilityFilter const todoApp combineReducers({ todos, visibilityFilter }) export default todoApp源码剖析combineReducers 在运行时做了什么文档说combineReducers替我们做了样板工作但它实际还内置了一组帮助初学者避坑的校验逻辑。下面结合 src/combineReducers.ts 的实现逐条对照。1. 过滤非函数值并告警combineReducers首先遍历传入对象的键只有typeof reducers[key] function的值才会进入finalReducers在开发环境下process.env.NODE_ENV ! production若某个键的值是undefined会输出No reducer provided for key xxx的 warning见 src/combineReducers.ts。对应的测试见 test/combineReducers.spec.tsignores all props which are not a function与warns if a reducer prop is undefined两个用例分别验证了非函数属性被忽略和undefined 触发警告。2. 两把探针检查 reducer 契约assertReducerShape见 src/combineReducers.ts会对每个 slice reducer 做两次探针调用reducer(undefined, { type: redux/INIT })如果返回undefined抛出错误——slice reducer 在初始化时返回了 undefined……当传入的 state 为 undefined 时必须显式返回初始 state。初始 state 不能是 undefined。这解释了教程中反复强调的(state initialState, action) 默认参数写法store 初始化时就会以undefined调用每个 reducerreducer(undefined, { type: 随机探针 type })如果返回undefined抛出错误——不要用 slice reducer 处理redux/*命名空间下的redux/INIT等私有 action对任何未知 action 你必须返回当前 state若 state 为 undefined 则返回初始 state。这两条校验对应了 docs/api/combineReducers.md Notes 一节列出的规则未识别的 action 必须原样返回state永远不能返回undefined收到undefinedstate 时必须返回该 reducer 的初始 state。测试 test/combineReducers.spec.ts 中的throws an error if a reducer returns undefined handling an action与throws an error on first call if a reducer returns undefined initializing用例验证了这两条路径。3. 未变化的 slice 不重建根 state组合 reducer 的核心循环见 src/combineReducers.ts对每个 key 依次执行reducer(previousStateForKey, action)并用引用比较判断变化nextState[key] nextStateForKey hasChanged hasChanged || nextStateForKey ! previousStateForKey // ... hasChanged hasChanged || finalReducerKeys.length ! Object.keys(state).length return hasChanged ? nextState : state也就是说只要某个 slice 对该 action不感兴趣而返回了原引用且没有任何 slice 的引用发生变化、键数量也没变组合 reducer 就直接返回旧的根 state 对象而不是新建一个根对象。这个短路行为有两个好处一是为上层如 React-Redux 的浅比较订阅保留了引用未变即无需重渲染的优化空间二是配合dispatch中currentState currentReducer(currentState, action)的赋值见 src/createStore.ts让无变化的 action 不产生新引用成为可能。另外若某个 slice 对某个 action 返回了undefined这里会抛出带 action type 与 key 名的详细错误the slice reducer for key xxx returned undefined...见 src/combineReducers.ts把忘记 return state这类错误尽早暴露。4. 开发环境下的 state 形状检查在开发环境组合 reducer 还会调用getUnexpectedStateShapeWarningMessage见 src/combineReducers.ts检查传入 state 的形状如果 state 不是纯对象借助 src/utils/isPlainObject.ts 判断或含有任何不在 reducer 键列表中的意外键会输出警告同一个意外键只警告一次通过unexpectedKeyCache去重redux/REPLACEaction 则被豁免。这对使用preloadedState、服务端渲染水合等场景尤其有帮助。5. 与 createStore 的联动INIT 与 dispatch 校验把视角拉回到 store。src/createStore.ts 在函数末尾执行dispatch({ type: ActionTypes.INIT })见 src/createStore.ts注释明确说明当 store 被创建时会派发一个 INIT action让每个 reducer 返回其初始 state从而填充初始 state 树。 这就是教程中reducer 可能以undefined被调用的来源也是combineReducers第一把探针用redux/INIT探测与之呼应的原因。dispatch本身则负责把action 必须是纯对象且带字符串 type这条契约落地见 src/createStore.ts非纯对象如 thunk 函数、Promise会报错提示可添加redux-thunk等中间件type为undefined或非字符串会报错在 reducer 执行期间再调用dispatch、getState、subscribe都会被禁止Reducers may not dispatch actions.等错误从机制上强化了reducer 是纯函数的规则。类型层面combineReducers的重载见 src/combineReducers.ts会基于传入的 reducers 映射推断出组合 state 类型StateFromReducersMapObjectM与 action 联合类型ActionFromReducersMapObjectM定义于 src/types/reducers.ts因此combineReducers({ todos: todosReducer, filters: filtersReducer })的返回类型会自动携带{ todos: Todo[], filters: Filters }这样的结构preloadedState也允许是Partial形状——这与state 结构由键名决定的运行时语义完全一致。小结State、Actions、Reducers 是 Redux 的基石每个 Redux 应用都有 state 值、用于描述发生了什么的 action、以及基于先前 state 与 action 计算新 state 的 reducer 函数。官方教程对本部分要点的归纳如下Redux 应用使用纯 JS 对象、数组与原始值作为 state 值根 state 值应是一个纯 JS 对象state 应只包含让应用工作所需的最小数据类、Promise、函数及其他非纯值不应该进入 Redux stateReducer 中不允许创建Math.random()、Date.now()这类随机值Redux store 之外完全可以并存其他 state如组件局部 state。Action 是带type字段的纯对象描述发生了什么type应是可读字符串通常写作feature/eventName如todos/todoAddedAction 可携带其他值通常放在action.payloadAction 应只携带描述发生了什么所需的最小数据。Reducer 形如(state, action) newState必须始终遵守只基于state与action参数计算新 state永远不 mutate 现有state始终返回副本不做 HTTP 请求、异步逻辑等副作用。Reducer 应当拆分以便阅读通常按顶层 state 键即 state 的切片拆分通常写在 slice 文件中按 feature 文件夹组织可以用 Redux 的combineReducers组合传给combineReducers的键名决定了顶层 state 对象的键。至此我们已经拥有了一套能更新 state 的 reducer 逻辑但这些 reducer 本身不会做任何事——它们需要放进一个 Redux store由 store 在事件发生时带着 action 调用它们。下一篇 Part 4Store见 part-4将讲解如何创建 Redux store 并让 reducer 逻辑真正跑起来。延伸阅读仓库内路径src/combineReducers.tscombineReducers完整实现含校验、短路逻辑src/createStore.tscreateStore实现INIT 派发、dispatch 校验、getState/subscribe/replaceReducersrc/types/reducers.ts 与 src/types/actions.tsReducer、ReducersMapObject、Action等类型定义docs/api/combineReducers.mdcombineReducersAPI 文档规则、参数与示例test/combineReducers.spec.ts上述运行时校验行为的测试用例examples/todos-with-undocombineReducers组合todosvisibilityFilter两个 slice 的最小完整示例教程上下文Part 2: 概念与数据流、Part 4: Store、Part 8: Modern Redux with Redux Toolkit。【免费下载链接】reduxA JS library for predictable global state management项目地址: https://gitcode.com/gh_mirrors/re/redux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考