Skip to main content

Redux 基础知识,第 8 部分:使用 Redux 工具包的现代 Redux

你将学到什么
  • 如何使用 Redux Toolkit 简化 Redux 逻辑

    ¥How to simplify your Redux logic using Redux Toolkit

  • 学习和使用 Redux 的后续步骤

    ¥Next steps for learning and using Redux

恭喜,你已完成本教程的最后一部分!在结束之前我们还有一个主题要讨论。

¥Congratulations, you've made it to the last section of this tutorial! We've got one more topic to cover before we're done.

如果你想了解我们迄今为止所介绍的内容,请查看以下摘要:

¥If you'd like a reminder of what we've covered so far, take a look at this summary:

信息

Recap: What You've Learned

  • 第 1 部分:概述

    ¥Part 1: Overview:

    • Redux 是什么、何时/为何使用它以及 Redux 应用的基本部分

      ¥what Redux is, when/why to use it, and the basic pieces of a Redux app

  • 第 2 部分:概念和数据流

    ¥Part 2: Concepts and Data Flow:

    • Redux 如何使用 "单向数据流" 模式

      ¥How Redux uses a "one-way data flow" pattern

  • 第 3 部分:状态、操作和 reducer

    ¥Part 3: State, Actions, and Reducers:

    • Redux 状态由纯 JS 数据组成

      ¥Redux state is made of plain JS data

    • 操作是描述应用中 "发生了什么" 事件的对象

      ¥Actions are objects that describe "what happened" events in an app

    • reducer 采用当前状态和操作,并计算新状态

      ¥Reducers take current state and an action, and calculate a new state

    • reducer 必须遵循 "不可变的更新" 和 "无副作用" 等规则

      ¥Reducers must follow rules like "immutable updates" and "no side effects"

  • 第 4 部分:存储

    ¥Part 4: Store:

    • createStore API 创建具有根 reducer 功能的 Redux 存储

      ¥The createStore API creates a Redux store with a root reducer function

    • 可以使用 "enhancers" 和 "中间件" 自定义存储

      ¥Stores can be customized using "enhancers" and "middleware"

    • Redux DevTools 扩展可让你查看状态如何随时间变化

      ¥The Redux DevTools extension lets you see how your state changes over time

  • 第 5 部分:UI 和 React

    ¥Part 5: UI and React:

    • Redux 独立于任何 UI,但经常与 React 一起使用

      ¥Redux is separate from any UI, but frequently used with React

    • React-Redux 提供 API 让 React 组件与 Redux 存储对话

      ¥React-Redux provides APIs to let React components talk to Redux stores

    • useSelector 从 Redux 状态读取值并订阅更新

      ¥useSelector reads values from Redux state and subscribes to updates

    • useDispatch 让组件分派动作

      ¥useDispatch lets components dispatch actions

    • <Provider> 封装你的应用并让组件访问存储

      ¥<Provider> wraps your app and lets components access the store

  • 第 6 部分:异步逻辑和数据获取

    ¥Part 6: Async Logic and Data Fetching:

    • Redux 中间件允许编写有副作用的逻辑

      ¥Redux middleware allow writing logic that has side effects

    • 中间件向 Redux 数据流添加了额外的步骤,支持异步逻辑

      ¥Middleware add an extra step to the Redux data flow, enabling async logic

    • Redux "thunk" 函数是编写基本异步逻辑的标准方法

      ¥Redux "thunk" functions are the standard way to write basic async logic

  • 第 7 部分:标准 Redux 模式

    ¥Part 7: Standard Redux Patterns:

    • 动作创建者封装了准备动作对象和 thunk

      ¥Action creators encapsulate preparing action objects and thunks

    • 记忆选择器优化计算转换后的数据

      ¥Memoized selectors optimize calculating transformed data

    • 应使用加载状态枚举值来跟踪请求状态

      ¥Request status should be tracked with loading state enum values

    • 规范化状态使得通过 ID 查找项目变得更加容易

      ¥Normalized state makes it easier to look up items by IDs

正如你所看到的,Redux 的许多方面都涉及编写一些可能很冗长的代码,例如不可变更新、操作类型和操作创建者以及规范化状态。这些模式的存在有充分的理由,但编写代码 "用手" 可能很困难。此外,设置 Redux 存储的过程需要几个步骤,我们必须为诸如在 thunk 中分派 "loading" 操作或处理规范化数据之类的事情提出自己的逻辑。最后,很多时候用户并不确定 "正确的方式" 是什么来编写 Redux 逻辑。

¥As you've seen, many aspects of Redux involve writing some code that can be verbose, such as immutable updates, action types and action creators, and normalizing state. There's good reasons why these patterns exist, but writing that code "by hand" can be difficult. In addition, the process for setting up a Redux store takes several steps, and we've had to come up with our own logic for things like dispatching "loading" actions in thunks or processing normalized data. Finally, many times users aren't sure what "the right way" is to write Redux logic.

这就是 Redux 团队创建 Redux 工具包:我们官方的、有态度的 "包括适配器" 工具集,用于高效 Redux 开发 的原因。

¥That's why the Redux team created Redux Toolkit: our official, opinionated, "batteries included" toolset for efficient Redux development.

Redux Toolkit 包含我们认为构建 Redux 应用所必需的包和函数。Redux Toolkit 构建了我们建议的最佳实践,简化了大多数 Redux 任务,防止常见错误,并使编写 Redux 应用变得更加容易。

¥Redux Toolkit contains packages and functions that we think are essential for building a Redux app. Redux Toolkit builds in our suggested best practices, simplifies most Redux tasks, prevents common mistakes, and makes it easier to write Redux applications.

正因为如此,Redux Toolkit 是编写 Redux 应用逻辑的标准方式。到目前为止,你在本教程中编写的 "hand-written" Redux 逻辑是实际工作代码,但你不应该手动编写 Redux 逻辑 - 我们在本教程中介绍了这些方法,以便你了解 Redux 的工作原理。但是,对于实际应用,你应该使用 Redux Toolkit 来编写 Redux 逻辑。

¥Because of this, Redux Toolkit is the standard way to write Redux application logic. The "hand-written" Redux logic you've written so far in this tutorial is actual working code, but you shouldn't write Redux logic by hand - we've covered those approaches in this tutorial so that you understand how Redux works. However, for real applications, you should use Redux Toolkit to write your Redux logic.

当你使用 Redux Toolkit 时,我们迄今为止介绍的所有概念(操作、reducer、存储设置、操作创建者、thunk 等)仍然存在,但 Redux Toolkit 提供了更简单的方法来编写该代码。

¥When you use Redux Toolkit, all the concepts that we've covered so far (actions, reducers, store setup, action creators, thunks, etc) still exist, but Redux Toolkit provides easier ways to write that code.

提示

Redux Toolkit 仅涵盖 Redux 逻辑 - 我们仍然使用 React-Redux 让我们的 React 组件与 Redux 存储对话,包括 useSelectoruseDispatch

¥Redux Toolkit only covers the Redux logic - we still use React-Redux to let our React components talk to the Redux store, including useSelector and useDispatch.

那么,让我们看看如何使用 Redux Toolkit 来简化我们在示例 todo 应用中编写的代码。我们将主要重写 "slice" 文件,但我们应该能够保持所有 UI 代码相同。

¥So, let's see how we can use Redux Toolkit to simplify the code we've already written in our example todo application. We'll primarily be rewriting our "slice" files, but we should be able to keep all the UI code the same.

在继续之前,将 Redux Toolkit 包添加到你的应用中:

¥Before we continue, add the Redux Toolkit package to your app:

npm install @reduxjs/toolkit

存储设置

¥Store Setup

我们已经对 Redux 存储的设置逻辑进行了几次迭代。目前,它看起来像这样:

¥We've gone through a few iterations of setup logic for our Redux store. Currently, it looks like this:

src/rootReducer.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
src/store.js
import { createStore, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension'
import rootReducer from './reducer'

const composedEnhancer = composeWithDevTools(applyMiddleware(thunkMiddleware))

const store = createStore(rootReducer, composedEnhancer)
export default store

请注意,设置过程需要几个步骤。我们必须:

¥Notice that the setup process takes several steps. We have to:

  • 将切片 reducer 组合在一起形成根 reducer

    ¥Combine the slice reducers together to form the root reducer

  • 将根 reducer 导入到 store 文件中

    ¥Import the root reducer into the store file

  • 导入 thunk 中间件、applyMiddlewarecomposeWithDevTools API

    ¥Import the thunk middleware, applyMiddleware, and composeWithDevTools APIs

  • 使用中间件和开发工具创建存储增强器

    ¥Create a store enhancer with the middleware and devtools

  • 使用根 reducer 创建存储

    ¥Create the store with the root reducer

如果我们能减少这里的步骤就好了。

¥It would be nice if we could cut down the number of steps here.

使用 configureStore

¥Using configureStore

Redux Toolkit 有一个 configureStore API,可以简化存储设置过程。configureStore 封装了 Redux 核心 createStore API,并自动为我们处理大部分存储设置。事实上,我们可以有效地将其简化为一步:

¥Redux Toolkit has a configureStore API that simplifies the store setup process. configureStore wraps around the Redux core createStore API, and handles most of the store setup for us automatically. In fact, we can cut it down to effectively one step:

src/store.js
import { configureStore } from '@reduxjs/toolkit'

import todosReducer from './features/todos/todosSlice'
import filtersReducer from './features/filters/filtersSlice'

const store = configureStore({
reducer: {
// Define a top-level state field named `todos`, handled by `todosReducer`
todos: todosReducer,
filters: filtersReducer
}
})

export default store

configureStore 的一次调用为我们完成了所有工作:

¥That one call to configureStore did all the work for us:

  • 它将 todosReducerfiltersReducer 组合到根 reducer 函数中,该函数将处理看起来像 {todos, filters} 的根状态

    ¥It combined todosReducer and filtersReducer into the root reducer function, which will handle a root state that looks like {todos, filters}

  • 它使用该根 reducer 创建了一个 Redux 存储

    ¥It created a Redux store using that root reducer

  • 它自动添加了 thunk 中间件

    ¥It automatically added the thunk middleware

  • 它会自动添加更多中间件来检查常见错误,例如意外改变状态

    ¥It automatically added more middleware to check for common mistakes like accidentally mutating the state

  • 它会自动设置 Redux DevTools Extension 连接

    ¥It automatically set up the Redux DevTools Extension connection

我们可以通过打开我们的示例待办事项应用并使用它来确认它是否有效。我们所有现有的功能代码都可以正常工作!由于我们正在调度操作、调度 thunk、读取 UI 中的状态以及查看 DevTools 中的操作历史记录,因此所有这些部分都必须正常工作。我们所做的就是关闭存储设置代码。

¥We can confirm this works by opening up our example todo application and using it. All of our existing feature code works fine! Since we're dispatching actions, dispatching thunks, reading state in the UI, and looking at the action history in the DevTools, all those pieces must be working correctly. All we've done is switched out the store setup code.

让我们看看如果我们不小心改变了某些状态会发生什么。如果我们更改 "待办事项加载" reducer,使其直接更改状态字段,而不是一成不变地制作副本,会怎么样?

¥Let's see what happens now if we accidentally mutate some of the state. What if we change the "todos loading" reducer so that it directly changes the state field, instead of immutably making a copy?

src/features/todos/todosSlice
export default function todosReducer(state = initialState, action) {
switch (action.type) {
// omit other cases
case 'todos/todosLoading': {
// ❌ WARNING: example only - don't do this in a normal reducer!
state.status = 'loading'
return state
}
default:
return state
}
}

呃哦。我们的整个应用崩溃了!发生了什么?

¥Uh-oh. Our whole app just crashed! What happened?

Immutability check middleware error

这个错误信息是一件好事 - 我们在应用中发现了一个错误!configureStore 特别添加了一个额外的中间件,只要它看到我们的状态发生意外突变(仅在开发模式下),就会自动抛出错误。这有助于发现我们在编写代码时可能犯的错误。

¥This error message is a good thing - we caught a bug in our app! configureStore specifically added an extra middleware that automatically throws an error whenever it sees an accidental mutation of our state (in development mode only). That helps catch mistakes we might make while writing our code.

包清理

¥Package Cleanup

Redux Toolkit 已经包含了我们正在使用的几个包,例如 reduxredux-thunkreselect,并重新导出了这些 API。所以,我们可以稍微清理一下我们的项目。

¥Redux Toolkit already includes several of the packages we're using, like redux, redux-thunk, and reselect, and re-exports those APIs. So, we can clean up our project a bit.

首先,我们可以将 createSelector 导入切换为来自 '@reduxjs/toolkit' 而不是 'reselect'。然后,我们可以删除 package.json 中列出的单独软件包:

¥First, we can switch our createSelector import to be from '@reduxjs/toolkit' instead of 'reselect'. Then, we can remove the separate packages we have listed in our package.json:

npm uninstall redux redux-thunk reselect

需要明确的是,我们仍在使用这些软件包并且需要安装它们。但是,由于 Redux Toolkit 依赖于它们,因此当我们安装 @reduxjs/toolkit 时它们会自动安装,因此我们不需要在 package.json 文件中专门列出其他包。

¥To be clear, we're still using these packages and need to have them installed. However, because Redux Toolkit depends on them, they'll be installed automatically when we install @reduxjs/toolkit, so we don't need to have the other packages specifically listed in our package.json file.

写入切片

¥Writing Slices

随着我们向应用添加新功能,切片文件变得更大、更复杂。特别是,由于所有嵌套对象都用于不可变更新,因此 todosReducer 变得更难以阅读,并且我们编写了多个动作创建器函数。

¥As we've added new features to our app, the slice files have gotten bigger and more complicated. In particular, the todosReducer has gotten harder to read because of all the nested object spreads for immutable updates, and we've written multiple action creator functions.

Redux Toolkit 有一个 createSlice API,它将帮助我们简化 Redux reducer 逻辑和操作。createSlice 为我们做了几件重要的事情:

¥Redux Toolkit has a createSlice API that will help us simplify our Redux reducer logic and actions. createSlice does several important things for us:

  • 我们可以将 case 缩减器编写为对象内部的函数,而不必编写 switch/case 语句

    ¥We can write the case reducers as functions inside of an object, instead of having to write a switch/case statement

  • reducer 将能够编写更短的不可变更新逻辑

    ¥The reducers will be able to write shorter immutable update logic

  • 所有的动作创建者将根据我们提供的 reducer 函数自动生成

    ¥All the action creators will be generated automatically based on the reducer functions we've provided

使用 createSlice

¥Using createSlice

createSlice 采用具有三个主要选项字段的对象:

¥createSlice takes an object with three main options fields:

  • name:将用作生成的操作类型的前缀的字符串

    ¥name: a string that will be used as the prefix for generated action types

  • initialState:reducer 的初始状态

    ¥initialState: the initial state of the reducer

  • reducers:一个对象,其中键是字符串,值是将处理特定操作的 "案例 reducer" 函数

    ¥reducers: an object where the keys are strings, and the values are "case reducer" functions that will handle specific actions

让我们首先看一个小的独立示例。

¥Let's look at a small standalone example first.

createSlice example
import { createSlice } from '@reduxjs/toolkit'

const initialState = {
entities: [],
status: null
}

const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
todoAdded(state, action) {
// ✅ This "mutating" code is okay inside of createSlice!
state.entities.push(action.payload)
},
todoToggled(state, action) {
const todo = state.entities.find(todo => todo.id === action.payload)
todo.completed = !todo.completed
},
todosLoading(state, action) {
return {
...state,
status: 'loading'
}
}
}
})

export const { todoAdded, todoToggled, todosLoading } = todosSlice.actions

export default todosSlice.reducer

在这个例子中,有几件事值得一看:

¥There's several things to see in this example:

  • 我们在 reducers 对象中编写大小写缩减函数,并给它们提供可读的名称

    ¥We write case reducer functions inside the reducers object, and give them readable names

  • createSlice 将自动生成对应于我们提供的每个案例 reducer 功能的动作创建者

    ¥createSlice will automatically generate action creators that correspond to each case reducer function we provide

  • createSlice 在默认情况下自动返回现有状态

    ¥createSlice automatically returns the existing state in the default case

  • createSlice 让我们能够安全地 "mutate" 我们的状态!

    ¥createSlice allows us to safely "mutate" our state!

  • 但是,如果我们愿意,我们也可以像以前一样制作不可变的副本

    ¥But, we can also make immutable copies like before if we want to

生成的动作创建器将作为 slice.actions.todoAdded 提供,我们通常会像之前编写的动作创建器一样单独解构和导出它们。完整的 reducer 功能可用作 slice.reducer,我们通常使用 export default slice.reducer,再次与以前相同。

¥The generated action creators will be available as slice.actions.todoAdded, and we typically destructure and export those individually like we did with the action creators we wrote earlier. The complete reducer function is available as slice.reducer, and we typically export default slice.reducer, again the same as before.

那么这些自动生成的动作对象是什么样的呢?让我们尝试调用其中一个并记录操作以查看:

¥So what do these auto-generated action objects look like? Let's try calling one of them and logging the action to see:

console.log(todoToggled(42))
// {type: 'todos/todoToggled', payload: 42}

createSlice 通过将切片的 name 字段与我们编写的缩减器函数的 todoToggled 名称相结合,为我们生成了操作类型字符串。默认情况下,动作创建者接受一个参数,并将其作为 action.payload 放入动作对象中。

¥createSlice generated the action type string for us, by combining the slice's name field with the todoToggled name of the reducer function we wrote. By default, the action creator accepts one argument, which it puts into the action object as action.payload.

在生成的 reducer 函数内部,createSlice 将检查已分派操作的 action.type 是否与其生成的名称之一匹配。如果是这样,它将运行该 case 缩减器函数。这与我们使用 switch/case 语句自己编写的模式完全相同,但 createSlice 自动为我们执行此操作。

¥Inside of the generated reducer function, createSlice will check to see if a dispatched action's action.type matches one of the names it generated. If so, it will run that case reducer function. This is exactly the same pattern that we wrote ourselves using a switch/case statement, but createSlice does it for us automatically.

"mutation" 方面也值得更详细地讨论。

¥It's also worth talking about the "mutation" aspect in more detail.

使用 Immer 进行不可变更新

¥Immutable Updates with Immer

之前,我们讨论了 "mutation"(修改现有对象/数组值)和 "immutability"(将值视为无法更改的内容)。

¥Earlier, we talked about "mutation" (modifying existing object/array values) and "immutability" (treating values as something that cannot be changed).

警告

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

¥In Redux, our reducers are never allowed to mutate the original / current state values!

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

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

¥So if we can't change the originals, how do we return an updated state?

提示

reducer 只能复制原始值,然后可以对副本进行修改。

¥Reducers can only make copies of the original values, and then they can mutate the copies.

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

正如你在本教程中所看到的,我们可以使用 JavaScript 的数组/对象扩展运算符和其他返回原始值副本的函数来手动编写不可变更新。然而,手动编写不可变的更新逻辑很困难,并且意外改变 reducer 中的状态是 Redux 用户最常犯的错误。

¥As you've seen throughout this tutorial, we can write immutable updates by hand by using JavaScript's array / object spread operators and other functions that return copies of the original values. However, writing immutable update logic by hand is hard, and accidentally mutating state in reducers is the single most common mistake Redux users make.

这就是为什么 Redux 工具包的 createSlice 功能可以让你以更简单的方式编写不可变更新!

¥That's why Redux Toolkit's createSlice function lets you write immutable updates an easier way!

createSlice 内部使用了一个名为 伊梅尔 的库。Immer 使用一种名为 Proxy 的特殊 JS 工具来封装你提供的数据,并让你编写 "mutates" 封装数据的代码。但是,Immer 会跟踪你尝试进行的所有更改,然后使用该更改列表返回一个安全的、不可变的更新值,就像你手动编写了所有不可变的更新逻辑一样。

¥createSlice uses a library called Immer inside. Immer uses a special JS tool called a Proxy to wrap the data you provide, and lets you write code that "mutates" that wrapped data. But, Immer tracks all the changes you've tried to make, and then uses that list of changes to return a safely immutably updated value, as if you'd written all the immutable update logic by hand.

所以,而不是这个:

¥So, instead of this:

function handwrittenReducer(state, action) {
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue
}
}
}
}
}

你可以编写如下所示的代码:

¥You can write code that looks like this:

function reducerWithImmer(state, action) {
state.first.second[action.someId].fourth = action.someValue
}

这更容易阅读!

¥That's a lot easier to read!

但是,请记住以下非常重要的事情:

¥But, here's something very important to remember:

警告

你只能在 Redux Toolkit 的 createSlicecreateReducer 中编写 "mutating" 逻辑,因为它们内部使用了 Immer!如果你在没有 Immer 的情况下在 reducer 中编写修改逻辑,它会改变状态并导致错误!

¥You can only write "mutating" logic in Redux Toolkit's createSlice and createReducer because they use Immer inside! If you write mutating logic in reducers without Immer, it will mutate the state and cause bugs!

Immer 仍然允许我们手动编写不可变的更新,并根据需要自行返回新值。你甚至可以混合搭配。例如,使用 array.filter() 从数组中删除一项通常更容易,因此你可以调用它,然后将结果分配给 state 到 "mutate":

¥Immer still lets us write immutable updates by hand and return the new value ourselves if we want to. You can even mix and match. For example, removing an item from an array is often easier to do with array.filter(), so you could call that and then assign the result to state to "mutate" it:

// can mix "mutating" and "immutable" code inside of Immer:
state.todos = state.todos.filter(todo => todo.id !== action.payload)

转换 Todos reducer

¥Converting the Todos Reducer

让我们开始将 todos 切片文件转换为使用 createSlice。我们将首先从 switch 语句中选择几个具体案例来展示该过程是如何工作的。

¥Let's start converting our todos slice file to use createSlice instead. We'll pick a couple specific cases from our switch statement first to show how the process works.

src/features/todos/todosSlice.js
import { createSlice } from '@reduxjs/toolkit'

const initialState = {
status: 'idle',
entities: {}
}

const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
todoAdded(state, action) {
const todo = action.payload
state.entities[todo.id] = todo
},
todoToggled(state, action) {
const todoId = action.payload
const todo = state.entities[todoId]
todo.completed = !todo.completed
}
}
})

export const { todoAdded, todoToggled } = todosSlice.actions

export default todosSlice.reducer

我们示例应用中的 todos reducer 仍然使用嵌套在父对象中的规范化状态,因此这里的代码与我们刚刚看到的微型 createSlice 示例有点不同。还记得我们是如何做到 编写大量嵌套的扩展运算符来更早地切换待办事项 的吗?现在,相同的代码变得更短且更易于阅读。

¥The todos reducer in our example app is still using normalized state that is nested in a parent object, so the code here is a bit different than the miniature createSlice example we just looked at. Remember how we had to write a lot of nested spread operators to toggle that todo earlier? Now that same code is a lot shorter and easier to read.

让我们向这个 reducer 添加更多案例。

¥Let's add a couple more cases to this reducer.

src/features/todos/todosSlice.js
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
todoAdded(state, action) {
const todo = action.payload
state.entities[todo.id] = todo
},
todoToggled(state, action) {
const todoId = action.payload
const todo = state.entities[todoId]
todo.completed = !todo.completed
},
todoColorSelected: {
reducer(state, action) {
const { color, todoId } = action.payload
state.entities[todoId].color = color
},
prepare(todoId, color) {
return {
payload: { todoId, color }
}
}
},
todoDeleted(state, action) {
delete state.entities[action.payload]
}
}
})

export const { todoAdded, todoToggled, todoColorSelected, todoDeleted } =
todosSlice.actions

export default todosSlice.reducer

todoAddedtodoToggled 的操作创建者只需要采用单个参数,例如整个待办事项对象或待办事项 ID。但是,如果我们需要传入多个参数,或者执行我们谈到的某些 "preparation" 逻辑(例如生成唯一 ID)怎么办?

¥The action creators for todoAdded and todoToggled only need to take a single parameter, like an entire todo object or a todo ID. But, what if we need to pass in multiple parameters, or do some of that "preparation" logic we talked about like generating a unique ID?

createSlice 让我们通过向 reducer 添加 "准备回调" 来处理这些情况。我们可以传递一个具有名为 reducerprepare 的函数的对象。当我们调用生成的动作创建者时,将使用传入的任何参数调用 prepare 函数。然后,它应该创建并返回一个具有 payload 字段(或者可选的 metaerror 字段)的对象,与 通量标准行动公约 匹配。

¥createSlice lets us handle those situations by adding a "prepare callback" to the reducer. We can pass an object that has functions named reducer and prepare. When we call the generated action creator, the prepare function will be called with whatever parameters were passed in. It should then create and return an object that has a payload field (or, optionally, meta and error fields), matching the Flux Standard Action convention.

在这里,我们使用了一个准备回调来让我们的 todoColorSelected 动作创建者接受单独的 todoIdcolor 参数,并将它们放在一起作为 action.payload 中的一个对象。

¥Here, we've used a prepare callback to let our todoColorSelected action creator accept separate todoId and color arguments, and put them together as an object in action.payload.

同时,在 todoDeleted reducer 中,我们可以使用 JS delete 运算符从规范化状态中删除项目。

¥Meanwhile, in the todoDeleted reducer, we can use the JS delete operator to remove items from our normalized state.

我们可以使用这些相同的模式来重写 todosSlice.jsfiltersSlice.js 中的其余 reducer。

¥We can use these same patterns to go rewrite the rest of our reducers in todosSlice.js and filtersSlice.js.

以下是我们的代码在所有切片均已转换后的样子:

¥Here's how our code looks with all the slices converted:

写感谢信

¥Writing Thunks

我们已经了解了如何做到 编写调度 "loading"、"请求成功" 和 "请求失败" 操作的 thunk。我们必须编写动作创建器、动作类型和缩减器来处理这些情况。

¥We've seen how we can write thunks that dispatch "loading", "request succeeded", and "request failed" actions. We had to write action creators, action types, and reducers to handle those cases.

由于这种模式非常常见,Redux Toolkit 有一个 createAsyncThunk API 可以为我们生成这些 thunk。它还为这些不同的请求状态操作生成操作类型和操作创建者,并根据生成的 Promise 自动分派这些操作。

¥Because this pattern is so common, Redux Toolkit has a createAsyncThunk API that will generate these thunks for us. It also generates the action types and action creators for those different request status actions, and dispatches those actions automatically based on the resulting Promise.

提示

Redux 工具包有一个新的 RTK 查询数据获取 API。RTK Query 是专为 Redux 应用构建的数据获取和缓存解决方案,无需编写任何 thunk 或 reducers 来管理数据获取。我们鼓励你尝试一下,看看它是否可以帮助简化你自己的应用中的数据获取代码!

¥Redux Toolkit has a new RTK Query data fetching API. RTK Query is a purpose built data fetching and caching solution for Redux apps, and can eliminate the need to write any thunks or reducers to manage data fetching. We encourage you to try it out and see if it can help simplify the data fetching code in your own apps!

我们将很快更新 Redux 教程,以包含有关使用 RTK 查询的部分。在此之前,请参阅 Redux Toolkit 文档中的 RTK 查询部分

¥We'll be updating the Redux tutorials soon to include sections on using RTK Query. Until then, see the RTK Query section in the Redux Toolkit docs.

使用 createAsyncThunk

¥Using createAsyncThunk

让我们通过生成 createAsyncThunk 的 thunk 来替换 fetchTodos thunk。

¥Let's replace our fetchTodos thunk by generating a thunk with createAsyncThunk.

createAsyncThunk 接受两个参数:

¥createAsyncThunk accepts two arguments:

  • 将用作生成的操作类型的前缀的字符串

    ¥A string that will be used as the prefix for the generated action types

  • 应返回 Promise 的 "有效负载创建者" 回调函数。这通常使用 async/await 语法编写,因为 async 函数自动返回一个 Promise。

    ¥A "payload creator" callback function that should return a Promise. This is often written using the async/await syntax, since async functions automatically return a promise.

src/features/todos/todosSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'

// omit imports and state

export const fetchTodos = createAsyncThunk('todos/fetchTodos', async () => {
const response = await client.get('/fakeApi/todos')
return response.todos
})

const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
// omit reducer cases
},
extraReducers: builder => {
builder
.addCase(fetchTodos.pending, (state, action) => {
state.status = 'loading'
})
.addCase(fetchTodos.fulfilled, (state, action) => {
const newEntities = {}
action.payload.forEach(todo => {
newEntities[todo.id] = todo
})
state.entities = newEntities
state.status = 'idle'
})
}
})

// omit exports

我们传递 'todos/fetchTodos' 作为字符串前缀,以及调用我们的 API 并返回包含所获取数据的 Promise 的 "有效负载创建者" 函数。在内部,createAsyncThunk 将生成三个动作创建者和动作类型,以及一个在调用时自动分派这些动作的 thunk 函数。在这种情况下,动作创建者及其类型是:

¥We pass 'todos/fetchTodos' as the string prefix, and a "payload creator" function that calls our API and returns a promise containing the fetched data. Inside, createAsyncThunk will generate three action creators and action types, plus a thunk function that automatically dispatches those actions when called. In this case, the action creators and their types are:

  • fetchTodos.pendingtodos/fetchTodos/pending

  • fetchTodos.fulfilledtodos/fetchTodos/fulfilled

  • fetchTodos.rejectedtodos/fetchTodos/rejected

但是,这些操作创建者和类型是在 createSlice 调用之外定义的。我们无法处理 createSlice.reducers 字段内的那些,因为它们也会生成新的操作类型。我们需要一种方法让 createSlice 调用监听其他地方定义的其他操作类型。

¥However, these action creators and types are being defined outside of the createSlice call. We can't handle those inside of the createSlice.reducers field, because those generate new action types too. We need a way for our createSlice call to listen for other action types that were defined elsewhere.

createSlice 还接受 extraReducers 选项,我们可以让相同的切片 reducer 监听其他操作类型。该字段应该是一个带有 builder 参数的回调函数,我们可以调用 builder.addCase(actionCreator, caseReducer) 来监听其他操作。

¥createSlice also accepts an extraReducers option, where we can have the same slice reducer listen for other action types. This field should be a callback function with a builder parameter, and we can call builder.addCase(actionCreator, caseReducer) to listen for other actions.

所以,这里我们称之为 builder.addCase(fetchTodos.pending, caseReducer)。当该操作被调度时,我们将运行设置 state.status = 'loading' 的 reducer,就像我们之前在 switch 语句中编写该逻辑时所做的那样。我们可以对 fetchTodos.fulfilled 做同样的事情,并处理我们从 API 收到的数据。

¥So, here we've called builder.addCase(fetchTodos.pending, caseReducer). When that action is dispatched, we'll run the reducer that sets state.status = 'loading', same as it did earlier when we wrote that logic in a switch statement. We can do the same thing for fetchTodos.fulfilled, and handle the data we received from the API.

再举一个例子,让我们转换 saveNewTodo。这个 thunk 将新的 todo 对象的 text 作为参数,并将其保存到服务器。我们该如何处理?

¥As one more example, let's convert saveNewTodo. This thunk takes the text of the new todo object as its parameter, and saves it to the server. How do we handle that?

src/features/todos/todosSlice.js
// omit imports

export const fetchTodos = createAsyncThunk('todos/fetchTodos', async () => {
const response = await client.get('/fakeApi/todos')
return response.todos
})

export const saveNewTodo = createAsyncThunk('todos/saveNewTodo', async text => {
const initialTodo = { text }
const response = await client.post('/fakeApi/todos', { todo: initialTodo })
return response.todo
})

const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
// omit case reducers
},
extraReducers: builder => {
builder
.addCase(fetchTodos.pending, (state, action) => {
state.status = 'loading'
})
.addCase(fetchTodos.fulfilled, (state, action) => {
const newEntities = {}
action.payload.forEach(todo => {
newEntities[todo.id] = todo
})
state.entities = newEntities
state.status = 'idle'
})
.addCase(saveNewTodo.fulfilled, (state, action) => {
const todo = action.payload
state.entities[todo.id] = todo
})
}
})

// omit exports and selectors

saveNewTodo 的过程与我们在 fetchTodos 中看到的过程相同。我们调用 createAsyncThunk,并传入操作前缀和有效负载创建者。在有效负载创建器中,我们进行异步 API 调用,并返回结果值。

¥The process for saveNewTodo is the same as we saw for fetchTodos. We call createAsyncThunk, and pass in the action prefix and a payload creator. Inside the payload creator, we make an async API call, and return a result value.

在这种情况下,当我们调用 dispatch(saveNewTodo(text)) 时,text 值将作为其第一个参数传递给有效负载创建者。

¥In this case, when we call dispatch(saveNewTodo(text)), the text value will be passed in to the payload creator as its first argument.

虽然我们不会在这里更详细地介绍 createAsyncThunk,但还有一些其他快速说明可供参考:

¥While we won't cover createAsyncThunk in more detail here, a few other quick notes for reference:

  • 当你调度 thunk 时,你只能将一个参数传递给它。如果需要传递多个值,请将它们传递到单个对象中

    ¥You can only pass one argument to the thunk when you dispatch it. If you need to pass multiple values, pass them in a single object

  • 有效负载创建者将接收一个对象作为其第二个参数,其中包含 {getState, dispatch} 和一些其他有用的值

    ¥The payload creator will receive an object as its second argument, which contains {getState, dispatch}, and some other useful values

  • thunk 在运行有效负载创建器之前调度 pending 操作,然后根据你返回的 Promise 成功还是失败来调度 fulfilledrejected

    ¥The thunk dispatches the pending action before running your payload creator, then dispatches either fulfilled or rejected based on whether the Promise you return succeeds or fails

正常化状态

¥Normalizing State

我们之前了解了如何通过将项目保存在由项目 ID 键入的对象中来进行 "normalize" 状态。这使我们能够通过 ID 查找任何项目,而无需循环遍历整个数组。然而,手动编写更新标准化状态的逻辑既漫长又乏味。使用 Immer 编写 "mutating" 更新代码会使事情变得更简单,但仍然可能会有很多重复 - 我们可能会在应用中加载许多不同类型的项目,并且每次都必须重复相同的 reducer 逻辑。

¥We previously saw how to "normalize" state, by keeping items in an object keyed by item IDs. This gives us the ability to look up any item by its ID without having to loop through an entire array. However, writing the logic to update normalized state by hand was long and tedious. Writing "mutating" update code with Immer makes that simpler, but there's still likely to be a lot of repetition - we might be loading many different types of items in our app, and we'd have to repeat the same reducer logic each time.

Redux Toolkit 包含一个 createEntityAdapter API,该 API 具有预构建的缩减器,用于具有规范化状态的典型数据更新操作。这包括添加、更新和删除切片中的项目。createEntityAdapter 还生成一些记忆选择器用于从存储中读取值。

¥Redux Toolkit includes a createEntityAdapter API that has prebuilt reducers for typical data update operations with normalized state. This includes adding, updating, and removing items from a slice. createEntityAdapter also generates some memoized selectors for reading values from the store.

使用 createEntityAdapter

¥Using createEntityAdapter

让我们用 createEntityAdapter 替换我们的规范化实体缩减器逻辑。

¥Let's replace our normalized entity reducer logic with createEntityAdapter.

调用 createEntityAdapter 给我们一个 "adapter" 对象,其中包含几个预制的 reducer 函数,包括:

¥Calling createEntityAdapter gives us an "adapter" object that contains several premade reducer functions, including:

  • addOne / addMany:向状态添加新项目

    ¥addOne / addMany: add new items to the state

  • upsertOne / upsertMany:添加新项目或更新现有项目

    ¥upsertOne / upsertMany: add new items or update existing ones

  • updateOne / updateMany:通过提供部分值来更新现有项目

    ¥updateOne / updateMany: update existing items by supplying partial values

  • removeOne / removeMany:根据 ID 删除项目

    ¥removeOne / removeMany: remove items based on IDs

  • setAll:替换所有现有项目

    ¥setAll: replace all existing items

我们可以使用这些函数作为大小写缩减器,或者作为 createSlice 内部的 "修改助手"。

¥We can use these functions as case reducers, or as "mutating helpers" inside of createSlice.

该适配器还包含:

¥The adapter also contains:

  • getInitialState:返回一个类似于 { ids: [], entities: {} } 的对象,用于存储项目的规范化状态以及所有项目 ID 的数组

    ¥getInitialState: returns an object that looks like { ids: [], entities: {} }, for storing a normalized state of items along with an array of all item IDs

  • getSelectors:生成一组标准的选择器函数

    ¥getSelectors: generates a standard set of selector functions

让我们看看如何在 todos 切片中使用它们:

¥Let's see how we can use these in our todos slice:

src/features/todos/todosSlice.js
import {
createSlice,
createAsyncThunk,
createEntityAdapter
} from '@reduxjs/toolkit'
// omit some imports

const todosAdapter = createEntityAdapter()

const initialState = todosAdapter.getInitialState({
status: 'idle'
})

// omit thunks

const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
// omit some reducers
// Use an adapter reducer function to remove a todo by ID
todoDeleted: todosAdapter.removeOne,
completedTodosCleared(state, action) {
const completedIds = Object.values(state.entities)
.filter(todo => todo.completed)
.map(todo => todo.id)
// Use an adapter function as a "mutating" update helper
todosAdapter.removeMany(state, completedIds)
}
},
extraReducers: builder => {
builder
.addCase(fetchTodos.pending, (state, action) => {
state.status = 'loading'
})
.addCase(fetchTodos.fulfilled, (state, action) => {
todosAdapter.setAll(state, action.payload)
state.status = 'idle'
})
// Use another adapter function as a reducer to add a todo
.addCase(saveNewTodo.fulfilled, todosAdapter.addOne)
}
})

// omit selectors

不同的适配器 reducer 函数根据函数的不同取不同的值,全部在 action.payload 中。"add" 和 "upsert" 函数采用单个项目或项目数组,"remove" 函数采用单个 ID 或 ID 数组,依此类推。

¥The different adapter reducer functions take different values depending on the function, all in action.payload. The "add" and "upsert" functions take a single item or an array of items, the "remove" functions take a single ID or array of IDs, and so on.

getInitialState 允许我们传递将包含的其他状态字段。在本例中,我们传入了 status 字段,为我们提供了最终的待办事项切片状态 {ids, entities, status},就像我们之前一样。

¥getInitialState allows us to pass in additional state fields that will be included. In this case, we've passed in a status field, giving us a final todos slice state of {ids, entities, status}, much like we had before.

我们还可以替换一些待办事项选择器功能。getSelectors 适配器函数将生成像 selectAll 这样的选择器,它返回所有项目的数组,以及 selectById,它返回一个项目。然而,由于 getSelectors 不知道我们的数据在整个 Redux 状态树中的位置,所以我们需要传入一个小选择器,从整个状态树中返回这个切片。让我们改用这些。由于这是对代码的最后一次重大更改,因此这次我们将包含整个 todos 切片文件,以查看使用 Redux Toolkit 的代码的最终版本是什么样子:

¥We can also replace some of our todos selector functions as well. The getSelectors adapter function will generate selectors like selectAll, which returns an array of all items, and selectById, which returns one item. However, since getSelectors doesn't know where our data is in the entire Redux state tree, we need to pass in a small selector that returns this slice out of the whole state tree. Let's switch to using these instead. Since this is the last major change to our code, we'll include the whole todos slice file this time to see what the final version of the code looks like using Redux Toolkit:

src/features/todos/todosSlice.js
import {
createSlice,
createSelector,
createAsyncThunk,
createEntityAdapter
} from '@reduxjs/toolkit'
import { client } from '../../api/client'
import { StatusFilters } from '../filters/filtersSlice'

const todosAdapter = createEntityAdapter()

const initialState = todosAdapter.getInitialState({
status: 'idle'
})

// Thunk functions
export const fetchTodos = createAsyncThunk('todos/fetchTodos', async () => {
const response = await client.get('/fakeApi/todos')
return response.todos
})

export const saveNewTodo = createAsyncThunk('todos/saveNewTodo', async text => {
const initialTodo = { text }
const response = await client.post('/fakeApi/todos', { todo: initialTodo })
return response.todo
})

const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
todoToggled(state, action) {
const todoId = action.payload
const todo = state.entities[todoId]
todo.completed = !todo.completed
},
todoColorSelected: {
reducer(state, action) {
const { color, todoId } = action.payload
state.entities[todoId].color = color
},
prepare(todoId, color) {
return {
payload: { todoId, color }
}
}
},
todoDeleted: todosAdapter.removeOne,
allTodosCompleted(state, action) {
Object.values(state.entities).forEach(todo => {
todo.completed = true
})
},
completedTodosCleared(state, action) {
const completedIds = Object.values(state.entities)
.filter(todo => todo.completed)
.map(todo => todo.id)
todosAdapter.removeMany(state, completedIds)
}
},
extraReducers: builder => {
builder
.addCase(fetchTodos.pending, (state, action) => {
state.status = 'loading'
})
.addCase(fetchTodos.fulfilled, (state, action) => {
todosAdapter.setAll(state, action.payload)
state.status = 'idle'
})
.addCase(saveNewTodo.fulfilled, todosAdapter.addOne)
}
})

export const {
allTodosCompleted,
completedTodosCleared,
todoAdded,
todoColorSelected,
todoDeleted,
todoToggled
} = todosSlice.actions

export default todosSlice.reducer

export const { selectAll: selectTodos, selectById: selectTodoById } =
todosAdapter.getSelectors(state => state.todos)

export const selectTodoIds = createSelector(
// First, pass one or more "input selector" functions:
selectTodos,
// Then, an "output selector" that receives all the input results as arguments
// and returns a final result value
todos => todos.map(todo => todo.id)
)

export const selectFilteredTodos = createSelector(
// First input selector: all todos
selectTodos,
// Second input selector: all filter values
state => state.filters,
// Output selector: receives both values
(todos, filters) => {
const { status, colors } = filters
const showAllCompletions = status === StatusFilters.All
if (showAllCompletions && colors.length === 0) {
return todos
}

const completedStatus = status === StatusFilters.Completed
// Return either active or completed todos based on filter
return todos.filter(todo => {
const statusMatches =
showAllCompletions || todo.completed === completedStatus
const colorMatches = colors.length === 0 || colors.includes(todo.color)
return statusMatches && colorMatches
})
}
)

export const selectFilteredTodoIds = createSelector(
// Pass our other memoized selector as an input
selectFilteredTodos,
// And derive data in the output selector
filteredTodos => filteredTodos.map(todo => todo.id)
)

我们调用 todosAdapter.getSelectors,并传入返回此状态切片的 state => state.todos 选择器。从那里,适配器生成一个 selectAll 选择器,它像往常一样获取整个 Redux 状态树,并循环 state.todos.entitiesstate.todos.ids 为我们提供完整的待办事项对象数组。由于 selectAll 没有告诉我们要选择什么,我们可以使用解构语法将函数重命名为 selectTodos。同样,我们可以将 selectById 重命名为 selectTodoById

¥We call todosAdapter.getSelectors, and pass in a state => state.todos selector that returns this slice of state. From there, the adapter generates a selectAll selector that takes the entire Redux state tree, as usual, and loops over state.todos.entities and state.todos.ids to give us the complete array of todo objects. Since selectAll doesn't tell us what we're selecting, we can use destructuring syntax to rename the function to selectTodos. Similarly, we can rename selectById to selectTodoById.

请注意,我们的其他选择器仍然使用 selectTodos 作为输入。这是因为它始终返回一个 todo 对象数组,无论我们是将数组保留为整个 state.todos、将其保留为嵌套数组,还是将其存储为规范化对象并转换为数组。即使我们对数据存储方式进行了所有这些更改,选择器的使用也使我们能够保持其余代码相同,并且记忆选择器的使用通过避免不必要的重新渲染帮助 UI 表现得更好。

¥Notice that our other selectors still use selectTodos as an input. That's because it's still returning an array of todo objects this whole time, no matter whether we were keeping the array as the entire state.todos, keeping it as a nested array, or storing it as a normalized object and converting to an array. Even as we've made all these changes to how we stored our data, the use of selectors allowed us to keep the rest of our code the same, and the use of memoized selectors has helped the UI perform better by avoiding unnecessary rerenders.

你学到了什么

¥What You've Learned

恭喜!你已完成 "Redux 基础知识" 教程!

¥Congratulations! You've completed the "Redux Fundamentals" tutorial!

你现在应该对 Redux 是什么、它是如何工作的以及如何正确使用它有了深入的了解:

¥You should now have a solid understanding of what Redux is, how it works, and how to use it correctly:

  • 管理全局应用状态

    ¥Managing global app state

  • 将应用的状态保持为纯 JS 数据

    ¥Keeping the state of our app as plain JS data

  • 在应用中编写描述 "发生了什么" 的操作对象

    ¥Writing action objects that describe "what happened" in the app

  • 使用 reducer 函数查看当前状态和操作,并创建一个不可变的新状态作为响应

    ¥Using reducer functions that look at the current state and an action, and create a new state immutably in response

  • 使用 useSelector 读取 React 组件中的 Redux 状态

    ¥Reading the Redux state in our React components with useSelector

  • 使用 useDispatch 从 React 组件分派操作

    ¥Dispatching actions from React components with useDispatch

此外,你还了解了 Redux Toolkit 如何简化 Redux 逻辑的编写,以及为什么 Redux Toolkit 是编写真正 Redux 应用的标准方法。通过首先了解如何编写 Redux 代码 "用手",你应该清楚像 createSlice 这样的 Redux Toolkit API 正在为你做什么,这样你就不必自己编写该代码。

¥In addition, you've seen how Redux Toolkit simplifies writing Redux logic, and why Redux Toolkit is the standard approach for writing real Redux applications. By seeing how to write Redux code "by hand" first, it should be clear what the Redux Toolkit APIs like createSlice are doing for you, so that you don't have to write that code yourself.

信息

有关 Redux Toolkit 的更多信息,包括使用指南和 API 参考,请参阅:

¥For more info on Redux Toolkit, including usage guides and API references, see:

让我们最后看一下已完成的待办事项应用,包括已转换为使用 Redux Toolkit 的所有代码:

¥Let's take one final look at the completed todo application, including all the code that's been converted to use Redux Toolkit:

我们将对你在本节中学到的要点进行最后的回顾:

¥And we'll do a final recap of the key points you learned in this section:

概括
  • Redux Toolkit (RTK) 是编写 Redux 逻辑的标准方式

    ¥Redux Toolkit (RTK) is the standard way to write Redux logic

    • RTK 包含可简化大多数 Redux 代码的 API

      ¥RTK includes APIs that simplify most Redux code

    • RTK 围绕 Redux 核心,并包含其他有用的包

      ¥RTK wraps around the Redux core, and includes other useful packages

  • configureStore 设置了一个具有良好默认值的 Redux 存储

    ¥configureStore sets up a Redux store with good defaults

    • 自动组合切片 reducer 以创建根 reducer

      ¥Automatically combines slice reducers to create the root reducer

    • 自动设置 Redux DevTools Extension 和调试中间件

      ¥Automatically sets up the Redux DevTools Extension and debugging middleware

  • createSlice 简化了 Redux 操作和化简器的编写

    ¥createSlice simplifies writing Redux actions and reducers

    • 根据切片/reducer 名称自动生成动作创建者

      ¥Automatically generates action creators based on slice/reducer names

    • reducer 可以使用 Immer 在 createSlice 内进行 "mutate" 状态

      ¥Reducers can "mutate" state inside createSlice using Immer

  • createAsyncThunk 为异步调用生成 thunk

    ¥createAsyncThunk generates thunks for async calls

    • 自动生成 thunk + pending/fulfilled/rejected 动作创建者

      ¥Automatically generates a thunk + pending/fulfilled/rejected action creators

    • 调度 thunk 会运行你的有效负载创建器并调度操作

      ¥Dispatching the thunk runs your payload creator and dispatches the actions

    • Thunk 动作可以在 createSlice.extraReducers 中处理

      ¥Thunk actions can be handled in createSlice.extraReducers

  • createEntityAdapter 提供归一化状态的 reducer + 选择器

    ¥createEntityAdapter provides reducers + selectors for normalized state

    • 包括用于添加/更新/删除项目等常见任务的 reducer 功能

      ¥Includes reducer functions for common tasks like adding/updating/removing items

    • 生成 selectAllselectById 的记忆选择器

      ¥Generates memoized selectors for selectAll and selectById

学习和使用 Redux 的后续步骤

¥Next Steps for Learning and Using Redux

现在你已经完成了本教程,我们为你接下来应该尝试什么来了解有关 Redux 的更多信息提供了一些建议。

¥Now that you've completed this tutorial, we have several suggestions for what you should try next to learn more about Redux.

这个 "基础知识" 教程重点关注 Redux 的底层方面:手动编写操作类型和不可变更新、Redux 存储和中间件如何工作,以及为什么我们使用操作创建者和规范化状态等模式。此外,我们的待办事项示例应用相当小,并不意味着作为构建完整应用的实际示例。

¥This "Fundamentals" tutorial focused on the low-level aspects of Redux: writing action types and immutable updates by hand, how a Redux store and middleware work, and why we use patterns like action creators and normalized state. In addition, our todo example app is fairly small, and not meant as a realistic example of building a full app.

然而,我们的 "Redux 要点" 教程 专门教你如何构建 "real-world" 类型的应用。它重点介绍使用 Redux Toolkit 的 "如何正确使用 Redux",并讨论你将在大型应用中看到的更现实的模式。它涵盖了许多与 "基础知识" 教程相同的主题,例如为什么 reducer 需要使用不可变更新,但重点是构建一个真正的工作应用。我们强烈建议你阅读 "Redux 要点" 教程作为下一步。

¥However, our "Redux Essentials" tutorial specifically teaches you how to build a "real-world" type application. It focuses on "how to use Redux the right way" using Redux Toolkit, and talks about more realistic patterns that you'll see in larger apps. It covers many of the same topics as this "Fundamentals" tutorial, such as why reducers need to use immutable updates, but with a focus on building a real working application. We strongly recommend reading through the "Redux Essentials" tutorial as your next step.

同时,我们在本教程中介绍的概念应该足以让你开始使用 React 和 Redux 构建自己的应用。现在是尝试自己开展项目以巩固这些概念并了解它们在实践中如何发挥作用的好时机。如果你不确定要构建什么样的项目,请参阅 这个应用项目创意列表 以获得一些灵感。

¥At the same time, the concepts we've covered in this tutorial should be enough to get you started building your own applications using React and Redux. Now's a great time to try working on a project yourself to solidify these concepts and see how they work in practice. If you're not sure what kind of a project to build, see this list of app project ideas for some inspiration.

使用 Redux 部分提供了有关许多重要概念的信息,例如 如何构造你的 reducer,而 我们的风格指南页面 部分提供了有关我们推荐的模式和最佳实践的重要信息。

¥The Using Redux section has information on a number of important concepts, like how to structure your reducers, and our Style Guide page has important information on our recommended patterns and best practices.

如果你想更多地了解 Redux 存在的原因、它试图解决哪些问题以及如何使用它,请参阅 Redux 维护者 Mark Erikson 在 Redux 之道,第 1 部分:实现和意图Redux 之道,第 2 部分:实践与理念 上的帖子。

¥If you'd like to know more about why Redux exists, what problems it tries to solve, and how it's meant to be used, see Redux maintainer Mark Erikson's posts on The Tao of Redux, Part 1: Implementation and Intent and The Tao of Redux, Part 2: Practice and Philosophy.

如果你正在寻求有关 Redux 问题的帮助,请加入 Discord 上 Reactiflux 服务器中的 #redux 通道

¥If you're looking for help with Redux questions, come join the #redux channel in the Reactiflux server on Discord.

感谢你阅读本教程,我们希望你喜欢使用 Redux 构建应用!

¥Thanks for reading through this tutorial, and we hope you enjoy building applications with Redux!