跳至主要内容

Redux 基础知识,第三部分:状态、动作和 Reducer

Redux 基础知识,第三部分:状态、动作和 Reducer

您将学到什么
  • 如何定义包含应用程序数据的状态值
  • 如何定义描述应用程序中发生事件的动作对象
  • 如何编写 Reducer 函数,根据现有状态和动作计算更新后的状态
先决条件
  • 熟悉 Redux 的关键术语和概念,例如“操作”(actions)、“reducers”、“store”和“dispatching”。(有关这些术语的解释,请参阅第 2 部分:Redux 概念和数据流。)

简介

第 2 部分:Redux 概念和数据流中,我们了解了 Redux 如何通过为我们提供一个存放全局应用程序状态的中心位置来帮助我们构建可维护的应用程序。我们还讨论了 Redux 的核心概念,例如分派操作对象和使用返回新状态值的 reducer 函数。

现在您已经对这些部分有所了解,是时候将这些知识付诸实践了。我们将构建一个小型示例应用程序,以了解这些部分是如何协同工作的。

注意

请注意,本教程有意展示旧式的 Redux 逻辑模式,这些模式需要比我们今天作为构建 Redux 应用程序的正确方法所教授的“现代 Redux”模式(使用 Redux Toolkit)更多的代码,以便解释 Redux 背后的原则和概念。它不是一个生产就绪的项目。

请参阅以下页面,了解如何使用 Redux Toolkit 的“现代 Redux”。

项目设置

在本教程中,我们创建了一个预先配置的入门项目,该项目已经设置了 React,包含一些默认样式,并具有一个假 REST API,它将允许我们在应用程序中编写实际的 API 请求。您将使用它作为编写实际应用程序代码的基础。

要开始,您可以打开并分叉此 CodeSandbox

您也可以 从这个 Github 仓库克隆相同的项目。克隆仓库后,您可以使用 npm install 安装项目的工具,并使用 npm start 启动它。

如果您想查看我们要构建的最终版本,您可以查看 tutorial-steps 分支,或 查看此 CodeSandbox 中的最终版本

创建新的 Redux + React 项目

完成本教程后,您可能想尝试在自己的项目上工作。我们建议使用 Create-React-App 的 Redux 模板 作为创建新的 Redux + React 项目的最快方法。它已经配置了 Redux Toolkit 和 React-Redux,使用 您在第 1 部分中看到的“计数器”应用程序示例的现代化版本。这样您就可以直接开始编写实际的应用程序代码,而无需添加 Redux 包并设置存储。

如果您想了解有关如何将 Redux 添加到项目的具体细节,请参阅此说明

详细说明:将 Redux 添加到 React 项目

CRA 的 Redux 模板已经配置了 Redux Toolkit 和 React-Redux。如果您从头开始设置一个新项目而没有该模板,请按照以下步骤操作

  • 添加 @reduxjs/toolkitreact-redux
  • 使用 RTK 的 configureStore API 创建 Redux 存储,并传入至少一个 reducer 函数
  • 将 Redux 存储导入到应用程序的入口点文件(例如 src/index.js)中
  • 使用 React-Redux 中的 <Provider> 组件包装您的根 React 组件,例如
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)

探索初始项目

此初始项目基于 标准 Create-React-App 项目模板,并进行了一些修改。

让我们快速了解一下初始项目包含的内容。

  • /src
    • index.js:应用程序的入口文件。它渲染主 <App> 组件。
    • App.js:主应用程序组件。
    • index.css:整个应用程序的样式
    • /api
      • client.js:一个小的 AJAX 请求客户端,允许我们进行 GET 和 POST 请求
      • server.js:为我们的数据提供一个假的 REST API。我们的应用程序稍后将从这些假的端点获取数据。
    • /exampleAddons:包含一些额外的 Redux 附加组件,我们将在本教程的后面使用它们来展示工作原理

如果您现在加载应用程序,您应该会看到一条欢迎消息,但应用程序的其余部分为空。

有了这些,让我们开始吧!

启动 Todo 示例应用程序

我们的示例应用程序将是一个小型“待办事项”应用程序。您可能以前见过待办事项应用程序示例 - 它们是不错的示例,因为它们让我们展示如何执行诸如跟踪项目列表、处理用户输入以及在数据更改时更新 UI 之类的事情,这些都是正常应用程序中发生的事情。

定义需求

让我们从确定此应用程序的初始业务需求开始。

  • UI 应包含三个主要部分
    • 一个输入框,让用户输入新待办事项的文本
    • 所有现有待办事项的列表
    • 一个页脚部分,显示未完成待办事项的数量,并显示过滤选项
  • 待办事项列表项应有一个复选框,用于切换其“已完成”状态。我们还应该能够为预定义的颜色列表添加颜色编码的类别标签,并删除待办事项。
  • 计数器应将活动待办事项的数量复数化:“0 项”、“1 项”、“3 项”等
  • 应有按钮将所有待办事项标记为已完成,以及通过删除所有已完成的待办事项来清除所有已完成的待办事项
  • 应有两种方法来过滤列表中显示的待办事项
    • 根据显示“全部”、“活动”和“已完成”待办事项进行过滤
    • 根据选择一种或多种颜色进行过滤,并显示其标签与这些颜色匹配的任何待办事项

我们稍后会添加更多需求,但这些已经足够我们开始。

最终目标是一个看起来像这样的应用程序

Example todo app screenshot

设计状态值

React 和 Redux 的核心原则之一是您的 UI 应该基于您的状态。因此,设计应用程序的一种方法是首先考虑描述应用程序工作方式所需的所有状态。尝试用尽可能少的 state 值来描述您的 UI 也是一个好主意,这样您需要跟踪和更新的数据更少。

从概念上讲,此应用程序有两个主要方面

  • 当前待办事项的实际列表
  • 当前过滤选项

我们还需要跟踪用户在“添加待办事项”输入框中输入的数据,但这不太重要,我们稍后会处理。

对于每个待办事项,我们需要存储一些信息

  • 用户输入的文本
  • 表示它是否已完成的布尔标志
  • 唯一的 ID 值
  • 如果已选择,则为颜色类别

我们的过滤行为可能可以用一些枚举值来描述

  • 已完成状态:“全部”、“活动”和“已完成”
  • 颜色:“红色”、“黄色”、“绿色”、“蓝色”、“橙色”、“紫色”

查看这些值,我们还可以说待办事项是“应用程序状态”(应用程序使用的核心数据),而过滤值是“UI 状态”(描述应用程序当前正在执行的操作的状态)。考虑这些不同类型的类别有助于理解不同状态部分的使用方式。

设计状态结构

在 Redux 中,我们的应用程序状态始终保存在纯 JavaScript 对象和数组中。这意味着您可能不会将其他东西放入 Redux 状态中 - 没有类实例、内置 JS 类型(如Map / Set / Promise / Date)、函数或任何其他不是纯 JS 数据的东西。

根 Redux 状态值几乎总是纯 JS 对象,其他数据嵌套在其中。

基于这些信息,我们现在应该能够描述在 Redux 状态中需要具有的值类型

  • 首先,我们需要一个待办事项对象数组。每个项目都应该具有以下字段
    • id:一个唯一的数字
    • text:用户输入的文本
    • completed:一个布尔标志
    • color:可选的颜色类别
  • 然后,我们需要描述我们的过滤选项。我们需要有
    • 当前的“已完成”过滤器值
    • 当前选定颜色类别的数组

所以,以下是一个示例,展示了我们应用程序的状态可能是什么样的。

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 之外拥有其他状态值是可以的!** 此示例目前足够小,因此我们实际上将所有状态都放在 Redux 存储中,但正如我们稍后将看到的那样,某些数据实际上不需要保存在 Redux 中(例如“此下拉菜单是否打开?”或“表单输入的当前值”)。

设计操作

**操作**是具有 `type` 字段的普通 JavaScript 对象。如前所述,**您可以将操作视为描述应用程序中发生的事情的事件**。

与我们根据应用程序需求设计状态结构的方式相同,我们也应该能够列出一些描述正在发生的事情的操作。

  • 根据用户输入的文本添加新的待办事项条目。
  • 切换待办事项的已完成状态。
  • 为待办事项选择颜色类别。
  • 删除待办事项。
  • 将所有待办事项标记为已完成。
  • 清除所有已完成的待办事项。
  • 选择不同的“已完成”过滤器值。
  • 添加新的颜色过滤器。
  • 删除颜色过滤器。

我们通常将描述正在发生的事情所需的任何额外数据放在 `action.payload` 字段中。这可以是数字、字符串或包含多个字段的对象。

Redux 存储不关心 `action.type` 字段的实际文本是什么。但是,您自己的代码将查看 `action.type` 以查看是否需要更新。此外,在调试时,您经常会在 Redux DevTools 扩展中查看操作类型字符串,以了解应用程序中发生了什么。因此,尝试选择可读且清楚描述正在发生的事情的操作类型 - 当您稍后查看它们时,更容易理解!

根据可以发生的事情列表,我们可以创建一个应用程序将使用的操作列表。

  • {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.payload字段中。我们可以将颜色过滤器行为分成两个操作,一个用于“添加”,另一个用于“删除”,但在这种情况下,我们将它作为一个操作,并在其中添加一个额外的字段,专门用于显示我们可以将对象作为操作负载。

与状态数据一样,操作应该包含描述发生的事情所需的最少信息

编写 Reducer

现在我们知道了状态结构和操作的样子,是时候编写第一个 reducer 了。

Reducer 是接受当前state 和一个action 作为参数,并返回一个新的state 结果的函数。换句话说,(state, action) => newState

创建根 Reducer

一个 Redux 应用程序实际上只有一个 reducer 函数:“根 reducer”函数,你将在稍后传递给createStore。这个根 reducer 函数负责处理所有分派的 action,并在每次计算整个新的 state 结果。

让我们从在src 文件夹中创建reducer.js 文件开始,与index.jsApp.js 并列。

每个 reducer 都需要一些初始状态,所以我们将添加一些假的 todo 条目来帮助我们开始。然后,我们可以编写 reducer 函数内部逻辑的概要。

src/reducer.js
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 doesn't recognize the action type, or doesn't
// care about this specific action, return the existing state unchanged
return state
}
}

当应用程序正在初始化时,reducer 可能会使用undefined 作为状态值被调用。如果发生这种情况,我们需要提供一个初始状态值,以便 reducer 代码的其余部分可以正常工作。Reducer 通常使用默认参数语法来提供初始状态:(state = initialState, action)

接下来,让我们添加处理'todos/todoAdded' 操作的逻辑。

首先,我们需要检查当前操作的类型是否与该特定字符串匹配。然后,我们需要返回一个包含所有状态的新对象,即使对于没有更改的字段也是如此。

src/reducer.js
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) {
// Do something here based on the different types of actions
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 doesn't recognize the action type, or doesn't
// care about this specific action, return the existing state unchanged
return state
}
}

这……为了向状态添加一个待办事项,要做的工作太多了。为什么需要做这么多额外的工作呢?

Reducer 的规则

我们之前说过,Reducer 必须始终遵循一些特殊规则

  • 它们应该只根据stateaction参数计算新的状态值
  • 它们不允许修改现有的state。相反,它们必须进行不可变更新,方法是复制现有的state并对复制的值进行更改。
  • 它们不能进行任何异步逻辑或其他“副作用”
提示

“副作用”是指任何可以在函数返回值之外看到的对状态或行为的更改。一些常见的副作用类型包括:

  • 将值记录到控制台
  • 保存文件
  • 设置异步计时器
  • 发出 AJAX HTTP 请求
  • 修改函数外部存在的某些状态,或修改函数参数
  • 生成随机数或唯一随机 ID(例如Math.random()Date.now()

任何遵循这些规则的函数也被称为“纯”函数,即使它不是专门编写为 Reducer 函数。

但是为什么这些规则很重要呢?有几个不同的原因

  • Redux 的目标之一是使您的代码可预测。当函数的输出仅根据输入参数计算时,更容易理解代码的工作原理以及如何测试它。
  • 另一方面,如果函数依赖于自身外部的变量,或表现随机,您永远不知道运行它时会发生什么。
  • 如果函数修改其他值,包括其参数,这可能会以意想不到的方式改变应用程序的工作方式。这可能是错误的常见来源,例如“我更新了状态,但现在我的 UI 在应该更新时没有更新!”
  • Redux DevTools 的一些功能依赖于您的 reducer 正确地遵循这些规则。

关于“不可变更新”的规则尤其重要,值得进一步讨论。

Reducer 和不可变更新

之前,我们讨论了“变异”(修改现有对象/数组的值)和“不可变性”(将值视为不可更改的东西)。

危险

在 Redux 中,我们的 reducer 绝不 允许变异原始/当前状态值!

// ❌ Illegal - by default, this will mutate the state!
state.value = 123

您在 Redux 中不能变异状态的原因有很多。

  • 它会导致错误,例如 UI 未正确更新以显示最新值。
  • 它使理解状态更新的原因和方式变得更加困难。
  • 它使编写测试变得更加困难。
  • 它破坏了正确使用“时间旅行调试”的能力。
  • 它违背了 Redux 的预期精神和使用模式。

那么,如果我们不能更改原始值,我们如何返回更新后的状态呢?

提示

Reducer 只能创建原始值的副本,然后它们可以变异这些副本。

// ✅ This is safe, because we made a copy
return {
...state,
value: 123
}

我们已经看到,我们可以手动编写不可变更新,方法是使用 JavaScript 的数组/对象展开运算符和其他返回原始值副本的函数。

当数据嵌套时,这会变得更加困难。不可变更新的关键规则是,您必须对需要更新的每个嵌套级别进行复制。

但是,如果您认为“以这种方式手动编写不可变更新看起来很难记住和正确执行”... 是的,您是对的!:)

手动编写不可变更新逻辑确实很困难,在 reducer 中意外变异状态是 Redux 用户最常见的错误。

提示

在实际应用中,您不必手动编写这些复杂的嵌套不可变更新。第 8 部分:使用 Redux Toolkit 的现代 Redux中,您将学习如何使用 Redux Toolkit 简化在 reducer 中编写不可变更新逻辑。

处理其他操作

考虑到这一点,让我们为另外几个情况添加 reducer 逻辑。首先,根据 ID 切换 todo 的 completed 字段。

src/reducer.js
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 isn't the todo item we're looking for, leave it alone
if (todo.id !== action.payload) {
return todo
}

// We've found the todo that has to change. Return a copy:
return {
...todo,
// Flip the completed flag
completed: !todo.completed
}
})
}
}
default:
return state
}
}

由于我们一直在关注 todos 状态,让我们添加一个 case 来处理“可见性选择已更改”操作。

src/reducer.js
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 {
...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 个操作,但这已经有点长了。如果我们尝试在这个 reducer 函数中处理所有操作,那么阅读它将变得很困难。

这就是为什么**通常将 reducer 拆分成多个较小的 reducer 函数** - 为了更容易理解和维护 reducer 逻辑。

拆分 Reducer

作为拆分的一部分,**Redux reducer 通常根据它们更新的 Redux 状态部分进行拆分**。我们的待办事项应用程序状态目前有两个顶级部分:state.todosstate.filters。因此,我们可以将大型根 reducer 函数拆分成两个较小的 reducer - 一个 todosReducer 和一个 filtersReducer

那么,这些拆分的 reducer 函数应该放在哪里呢?

**我们建议根据“功能”组织你的 Redux 应用程序文件夹和文件** - 与应用程序的特定概念或领域相关的代码。**特定功能的 Redux 代码通常写在一个单独的文件中,称为“切片”文件**,其中包含该应用程序状态部分的所有 reducer 逻辑和所有与操作相关的代码。

因此,**特定 Redux 应用程序状态部分的 reducer 被称为“切片 reducer”**。通常,一些操作对象与特定切片 reducer 密切相关,因此操作类型字符串应该以该功能的名称(如 'todos')开头,并描述发生的事件(如 'todoAdded'),组合成一个字符串('todos/todoAdded')。

在我们的项目中,创建一个新的 features 文件夹,然后在其中创建一个 todos 文件夹。创建一个名为 todosSlice.js 的新文件,并将与待办事项相关的初始状态剪切粘贴到此文件中。

src/features/todos/todosSlice.js
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
}
}

现在我们可以复制更新待办事项的逻辑。但是,这里有一个重要的区别。**此文件只需要更新与待办事项相关的状态 - 它不再嵌套了!** 这是我们拆分 reducer 的另一个原因。由于待办事项状态本身是一个数组,我们不必在这里复制外部根状态对象。这使得这个 reducer 更容易阅读。

这被称为**reducer 组合**,它是构建 Redux 应用程序的基本模式。

以下是处理这些操作后更新后的 reducer 的样子。

src/features/todos/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,并将所有与过滤器相关的代码移到那里。

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
}
}

我们仍然需要复制包含过滤器状态的对象,但由于嵌套较少,因此更容易阅读发生了什么。

信息

为了使此页面更短,我们将跳过显示如何为其他操作编写 reducer 更新逻辑。

尝试自己编写这些更新,基于 上面描述的要求

如果你卡住了,请查看 此页面末尾的 CodeSandbox,了解这些 reducer 的完整实现。

组合 Reducer

现在我们有两个独立的切片文件,每个文件都有自己的切片 reducer 函数。但是,我们之前说过,Redux 存储在创建时需要一个根 reducer 函数。那么,如何在不将所有代码放在一个大函数中的情况下回到拥有一个根 reducer 呢?

由于 reducer 是普通的 JS 函数,我们可以将切片 reducer 导入回 reducer.js,并编写一个新的根 reducer,它唯一的任务是调用另外两个函数。

src/reducer.js
import 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 都管理着全局状态的一部分。每个 reducer 的 state 参数都不同,对应于它管理的状态部分。

这使我们能够根据功能和状态切片来拆分逻辑,以保持可维护性。

combineReducers

我们可以看到,新的根 reducer 对每个切片都做了同样的事情:调用切片 reducer,传入该 reducer 拥有的状态切片,并将结果分配回根状态对象。如果我们要添加更多切片,模式将重复。

Redux 核心库包含一个名为 combineReducers 的实用程序,它可以为我们完成相同的样板步骤。我们可以用 combineReducers 生成的更短的代码替换我们手写的 rootReducer

现在我们需要 combineReducers,是时候实际安装 Redux 核心库了。:

npm install redux

完成后,我们可以导入 combineReducers 并使用它。

src/reducer.js
import { 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 rootReducer

combineReducers 接受一个对象,其中键名将成为根状态对象中的键,而值是知道如何更新 Redux 状态这些切片的切片 reducer 函数。

请记住,您赋予 combineReducers 的键名将决定状态对象的键名!

您学到了什么

状态、动作和 reducer 是 Redux 的构建块。每个 Redux 应用程序都有状态值,创建动作来描述发生了什么,并使用 reducer 函数根据先前状态和动作计算新的状态值。

以下是我们应用程序到目前为止的内容

总结
  • Redux 应用程序使用纯 JS 对象、数组和基本类型作为状态值。
    • 根状态值应该是一个纯 JS 对象。
    • 状态应该包含使应用程序正常运行所需的最小数据量。
    • 类、Promise、函数和其他非纯值应该放在 Redux 状态中。
    • reducer 必须不创建随机值,例如 Math.random()Date.now()
    • 与 Redux 并排使用其他不在 Redux 存储中的状态值(如本地组件状态)是可以的。
  • 动作是具有 type 字段的纯对象,描述了发生了什么。
    • type 字段应该是一个可读的字符串,通常写成 'feature/eventName'
    • 动作可能包含其他值,这些值通常存储在 action.payload 字段中。
    • 动作应该具有描述发生了什么所需的最小数据量。
  • Reducers 是类似于 (state, action) => newState 的函数。
    • Reducers 必须始终遵循特殊规则。
      • 仅根据 stateaction 参数计算新状态。
      • 永远不要修改现有的 state - 始终返回一个副本。
      • 没有“副作用”,例如 AJAX 调用或异步逻辑。
  • Reducers 应该被拆分,以便更容易阅读。
    • Reducers 通常根据顶层状态键或状态的“切片”进行拆分。
    • Reducers 通常写在“切片”文件中,组织成“功能”文件夹。
    • Reducers 可以使用 Redux 的 combineReducers 函数组合在一起。
    • combineReducers 的键名定义了顶层状态对象的键。

下一步?

我们现在有一些 reducer 逻辑可以更新我们的状态,但这些 reducer 本身不会做任何事情。它们需要放在 Redux store 中,当发生某些事情时,store 可以用 actions 调用 reducer 代码。

第 4 部分:Store 中,我们将了解如何创建 Redux store 并运行我们的 reducer 逻辑。