Skip to main content

Redux 要点,第 5 部分:异步逻辑和数据获取

你将学到什么
  • 如何使用 Redux "thunk" 中间件进行异步逻辑

    ¥How to use the Redux "thunk" middleware for async logic

  • 处理异步请求状态的模式

    ¥Patterns for handling async request state

  • 如何使用 Redux Toolkit createAsyncThunk API 来简化异步调用

    ¥How to use the Redux Toolkit createAsyncThunk API to simplify async calls

先决条件
  • 熟悉使用 AJAX 请求从服务器获取和更新数据

    ¥Familiarity with using AJAX requests to fetch and update data from a server

介绍

¥Introduction

第 4 部分:使用 Redux 数据 中,我们了解了如何在 React 组件内部使用来自 Redux 存储的多条数据,在分派操作对象之前自定义操作对象的内容,以及在我们的 reducer 中处理更复杂的更新逻辑。

¥In Part 4: Using Redux Data, we saw how to use multiple pieces of data from the Redux store inside of React components, customize the contents of action objects before they're dispatched, and handle more complex update logic in our reducers.

到目前为止,我们使用的所有数据都直接位于 React 客户端应用内部。然而,大多数实际应用需要通过调用 HTTP API 来获取和保存项目来处理来自服务器的数据。

¥So far, all the data we've worked with has been directly inside of our React client application. However, most real applications need to work with data from a server, by making HTTP API calls to fetch and save items.

在本部分中,我们将转换社交媒体应用以从 API 获取帖子和用户数据,并通过将新帖子保存到 API 来添加新帖子。

¥In this section, we'll convert our social media app to fetch the posts and users data from an API, and add new posts by saving them to the API.

提示

Redux 工具包包括 RTK 查询数据获取和缓存 API。RTK Query 是专为 Redux 应用构建的数据获取和缓存解决方案,无需编写任何 thunk 或 reducers 来管理数据获取。我们专门教授 RTK 查询作为数据获取的默认方法,并且 RTK 查询建立在本页所示的相同模式之上。

¥Redux Toolkit includes the RTK Query data fetching and caching 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 specifically teach RTK Query as the default approach for data fetching, and RTK Query is built on the same patterns shown in this page.

我们将从 第 7 部分:RTK 查询基础知识 开始介绍如何使用 RTK 查询。

¥We'll cover how to use RTK Query starting in Part 7: RTK Query Basics.

REST API 和客户端示例

¥Example REST API and Client

为了保持示例项目的独立性和现实性,初始项目设置已经包含了一个用于数据的虚拟内存中 REST API(使用 Mock Service Worker 模拟 API 工具 配置)。该 API 使用 /fakeApi 作为端点的基本 URL,并支持 /fakeApi/posts/fakeApi/usersfakeApi/notifications 的典型 GET/POST/PUT/DELETE HTTP 方法。它在 src/api/server.js 中定义。

¥To keep the example project isolated but realistic, the initial project setup already includes a fake in-memory REST API for our data (configured using the Mock Service Worker mock API tool). The API uses /fakeApi as the base URL for the endpoints, and supports the typical GET/POST/PUT/DELETE HTTP methods for /fakeApi/posts, /fakeApi/users, and fakeApi/notifications. It's defined in src/api/server.js.

该项目还包括一个小型 HTTP API 客户端对象,它公开 client.get()client.post() 方法,类似于 axios 等流行的 HTTP 库。它在 src/api/client.js 中定义。

¥The project also includes a small HTTP API client object that exposes client.get() and client.post() methods, similar to popular HTTP libraries like axios. It's defined in src/api/client.js.

在本节中,我们将使用 client 对象对内存中的虚假 REST API 进行 HTTP 调用。

¥We'll use the client object to make HTTP calls to our in-memory fake REST API for this section.

此外,模拟服务器已设置为每次加载页面时重用相同的随机种子,以便它将生成相同的假用户和假帖子列表。如果你想重置该功能,请删除浏览器本地存储中的 'randomTimestampSeed' 值并重新加载页面,或者你可以通过编辑 src/api/server.js 并将 useSeededRNG 设置为 false 来关闭该功能。

¥Also, the mock server has been set up to reuse the same random seed each time the page is loaded, so that it will generate the same list of fake users and fake posts. If you want to reset that, delete the 'randomTimestampSeed' value in your browser's Local Storage and reload the page, or you can turn that off by editing src/api/server.js and setting useSeededRNG to false.

信息

提醒一下,代码示例重点关注每个部分的关键概念和更改。请参阅 CodeSandbox 项目和 项目仓库中的 tutorial-steps 分支 以了解应用中的完整更改。

¥As a reminder, the code examples focus on the key concepts and changes for each section. See the CodeSandbox projects and the tutorial-steps branch in the project repo for the complete changes in the application.

块和异步逻辑

¥Thunks and Async Logic

使用中间件启用异步逻辑

¥Using Middleware to Enable Async Logic

Redux 存储本身并不了解异步逻辑。它只知道如何同步分派操作,通过调用根 reducer 函数更新状态,并通知 UI 某些内容发生了变化。任何异步性都必须发生在存储之外。

¥By itself, a Redux store doesn't know anything about async logic. It only knows how to synchronously dispatch actions, update the state by calling the root reducer function, and notify the UI that something has changed. Any asynchronicity has to happen outside the store.

但是,如果你希望通过分派或检查当前存储状态来使异步逻辑与存储交互,该怎么办?这就是 Redux 中间件 发挥作用的地方。他们扩展了存储,并允许你:

¥But, what if you want to have async logic interact with the store by dispatching or checking the current store state? That's where Redux middleware come in. They extend the store, and allow you to:

  • 分派任何操作时执行额外的逻辑(例如记录操作和状态)

    ¥Execute extra logic when any action is dispatched (such as logging the action and state)

  • 暂停、修改、延迟、替换或停止已调度的操作

    ¥Pause, modify, delay, replace, or halt dispatched actions

  • 编写可以访问 dispatchgetState 的额外代码

    ¥Write extra code that has access to dispatch and getState

  • dispatch 如何接受除普通操作对象之外的其他值,例如函数和 Promise,通过拦截它们并分派真实的操作对象

    ¥Teach dispatch how to accept other values besides plain action objects, such as functions and promises, by intercepting them and dispatching real action objects instead

使用中间件的最常见原因是允许不同类型的异步逻辑与存储交互。这允许你编写可以调度操作并检查存储状态的代码,同时将该逻辑与 UI 分开。

¥The most common reason to use middleware is to allow different kinds of async logic to interact with the store. This allows you to write code that can dispatch actions and check the store state, while keeping that logic separate from your UI.

Redux 有多种异步中间件,每种都允许你使用不同的语法编写逻辑。最常见的异步中间件是 redux-thunk,它允许你直接编写可能包含异步逻辑的普通函数。Redux Toolkit 的 configureStore 函数 默认自动设置 thunk 中间件我们建议使用 thunk 作为使用 Redux 编写异步逻辑的标准方法

¥There are many kinds of async middleware for Redux, and each lets you write your logic using different syntax. The most common async middleware is redux-thunk, which lets you write plain functions that may contain async logic directly. Redux Toolkit's configureStore function automatically sets up the thunk middleware by default, and we recommend using thunks as a standard approach for writing async logic with Redux.

早些时候,我们看到了 Redux 的同步数据流是什么样的。当我们引入异步逻辑时,我们添加了一个额外的步骤,中间件可以运行 AJAX 请求等逻辑,然后调度操作。这使得异步数据流看起来像这样:

¥Earlier, we saw what the synchronous data flow for Redux looks like. When we introduce asynchronous logic, we add an extra step where middleware can run logic like AJAX requests, then dispatch actions. That makes the async data flow look like this:

Redux async data flow diagram

块函数

¥Thunk Functions

一旦 thunk 中间件被添加到 Redux 存储中,它就允许你将 thunk 函数直接传递给 store.dispatch。thunk 函数将始终以 (dispatch, getState) 作为参数进行调用,你可以根据需要在 thunk 中使用它们。

¥Once the thunk middleware has been added to the Redux store, it allows you to pass thunk functions directly to store.dispatch. A thunk function will always be called with (dispatch, getState) as its arguments, and you can use them inside the thunk as needed.

Thunk 通常使用动作创建器来分派普通动作,例如 dispatch(increment())

¥Thunks typically dispatch plain actions using action creators, like dispatch(increment()):

const store = configureStore({ reducer: counterReducer })

const exampleThunkFunction = (dispatch, getState) => {
const stateBefore = getState()
console.log(`Counter before: ${stateBefore.counter}`)
dispatch(increment())
const stateAfter = getState()
console.log(`Counter after: ${stateAfter.counter}`)
}

store.dispatch(exampleThunkFunction)

为了与分派正常操作对象保持一致,我们通常将它们编写为 thunk 操作创建者,它返回 thunk 函数。这些动作创建者可以采用可在 thunk 内部使用的参数。

¥For consistency with dispatching normal action objects, we typically write these as thunk action creators, which return the thunk function. These action creators can take arguments that can be used inside the thunk.

const logAndAdd = amount => {
return (dispatch, getState) => {
const stateBefore = getState()
console.log(`Counter before: ${stateBefore.counter}`)
dispatch(incrementByAmount(amount))
const stateAfter = getState()
console.log(`Counter after: ${stateAfter.counter}`)
}
}

store.dispatch(logAndAdd(5))

thunk 通常写入 "slice" 文件中。createSlice 本身对定义 thunk 没有任何特殊支持,因此你应该将它们作为单独的函数编写在同一个切片文件中。这样,他们就可以访问该切片的简单动作创建者,并且很容易找到 thunk 所在的位置。

¥Thunks are typically written in "slice" files. createSlice itself does not have any special support for defining thunks, so you should write them as separate functions in the same slice file. That way, they have access to the plain action creators for that slice, and it's easy to find where the thunk lives.

信息

"thunk" 一词是一个编程术语,意思是 "一段执行一些延迟工作的代码"。有关如何使用 thunk 的更多详细信息,请参阅 thunk 使用指南页面:

¥The word "thunk" is a programming term that means "a piece of code that does some delayed work". For more details on how to use thunks, see the thunk usage guide page:

以及这些帖子:

¥as well as these posts:

编写异步 thunk

¥Writing Async Thunks

Thunk 内部可能有异步逻辑,例如 setTimeoutPromiseasync/await。这使得它们成为对服务器 API 进行 AJAX 调用的好地方。

¥Thunks may have async logic inside of them, such as setTimeout, Promises, and async/await. This makes them a good place to put AJAX calls to a server API.

Redux 的数据获取逻辑通常遵循可预测的模式:

¥Data fetching logic for Redux typically follows a predictable pattern:

  • 在请求之前调度 "start" 操作,以指示请求正在进行中。这可用于跟踪加载状态以允许跳过重复请求或在 UI 中显示加载指示器。

    ¥A "start" action is dispatched before the request, to indicate that the request is in progress. This may be used to track loading state to allow skipping duplicate requests or show loading indicators in the UI.

  • 发出异步请求

    ¥The async request is made

  • 根据请求结果,异步逻辑调度包含结果数据的 "success" 操作或包含错误详细信息的 "failure" 操作。reducer 逻辑会清除两种情况下的加载状态,并处理成功案例的结果数据,或存储错误值以供潜在显示。

    ¥Depending on the request result, the async logic dispatches either a "success" action containing the result data, or a "failure" action containing error details. The reducer logic clears the loading state in both cases, and either processes the result data from the success case, or stores the error value for potential display.

这些步骤不是必需的,但很常用。(如果你只关心成功结果,则可以在请求完成时仅分派单个 "success" 操作,并跳过 "start" 和 "failure" 操作。)

¥These steps are not required, but are commonly used. (If all you care about is a successful result, you can just dispatch a single "success" action when the request finishes, and skip the "start" and "failure" actions.)

Redux Toolkit 提供了 createAsyncThunk API 来实现这些操作的创建和分派,我们很快就会了解如何使用它。

¥Redux Toolkit provides a createAsyncThunk API to implement the creation and dispatching of these actions, and we'll look at how to use it shortly.

Detailed Explanation: Dispatching Request Status Actions in Thunks

如果我们手动编写典型的异步 thunk 代码,它可能如下所示:

¥If we were to write out the code for a typical async thunk by hand, it might look like this:

const getRepoDetailsStarted = () => ({
type: 'repoDetails/fetchStarted'
})
const getRepoDetailsSuccess = repoDetails => ({
type: 'repoDetails/fetchSucceeded',
payload: repoDetails
})
const getRepoDetailsFailed = error => ({
type: 'repoDetails/fetchFailed',
error
})
const fetchIssuesCount = (org, repo) => async dispatch => {
dispatch(getRepoDetailsStarted())
try {
const repoDetails = await getRepoDetails(org, repo)
dispatch(getRepoDetailsSuccess(repoDetails))
} catch (err) {
dispatch(getRepoDetailsFailed(err.toString()))
}
}

然而,使用这种方法编写代码很乏味。每个单独类型的请求都需要重复类似的实现:

¥However, writing code using this approach is tedious. Each separate type of request needs repeated similar implementation:

  • 需要为三种不同的情况定义独特的操作类型

    ¥Unique action types need to be defined for the three different cases

  • 每个动作类型通常都有相应的动作创建者函数

    ¥Each of those action types usually has a corresponding action creator function

  • 必须编写一个 thunk,以正确的顺序分派正确的操作

    ¥A thunk has to be written that dispatches the correct actions in the right sequence

createAsyncThunk 通过生成操作类型和操作创建者并生成自动分派这些操作的 thunk 来抽象此模式。你提供一个回调函数来进行异步调用并返回带有结果的 Promise。

¥createAsyncThunk abstracts this pattern by generating the action types and action creators, and generating a thunk that dispatches those actions automatically. You provide a callback function that makes the async call and returns a Promise with the result.


加载帖子

¥Loading Posts

到目前为止,我们的 postsSlice 已经使用了一些硬编码的样本数据作为其初始状态。我们将把它切换为从空的帖子数组开始,然后从服务器获取帖子列表。

¥So far, our postsSlice has used some hardcoded sample data as its initial state. We're going to switch that to start with an empty array of posts instead, and then fetch a list of posts from the server.

为此,我们必须更改 postsSlice 中的状态结构,以便我们可以跟踪 API 请求的当前状态。

¥In order to do that, we're going to have to change the structure of the state in our postsSlice, so that we can keep track of the current state of the API request.

提取帖子选择器

¥Extracting Posts Selectors

现在,postsSlice 状态是 posts 的单个数组。我们需要将其更改为具有 posts 数组以及加载状态字段的对象。

¥Right now, the postsSlice state is a single array of posts. We need to change that to be an object that has the posts array, plus the loading state fields.

同时,像 <PostsList> 这样的 UI 组件正在尝试在 useSelector 钩子中读取 state.posts 的帖子,假设该字段是一个数组。我们还需要更改这些位置以匹配新数据。

¥Meanwhile, the UI components like <PostsList> are trying to read posts from state.posts in their useSelector hooks, assuming that field is an array. We need to change those locations also to match the new data.

如果我们不必每次更改 reducer 中的数据格式时都必须重写组件,那就太好了。避免这种情况的一种方法是在切片文件中定义可重用的选择器函数,并让组件使用这些选择器来提取所需的数据,而不是在每个组件中重复选择器逻辑。这样,如果我们再次更改状态结构,我们只需要更新切片文件中的代码。

¥It would be nice if we didn't have to keep rewriting our components every time we made a change to the data format in our reducers. One way to avoid this is to define reusable selector functions in the slice files, and have the components use those selectors to extract the data they need instead of repeating the selector logic in each component. That way, if we do change our state structure again, we only need to update the code in the slice file.

<PostsList> 组件需要读取所有帖子的列表,<SinglePostPage><EditPostForm> 组件需要通过 ID 查找单个帖子。让我们从 postsSlice.js 导出两个小的选择器函数来覆盖这些情况:

¥The <PostsList> component needs to read a list of all the posts, and the <SinglePostPage> and <EditPostForm> components need to look up a single post by its ID. Let's export two small selector functions from postsSlice.js to cover those cases:

features/posts/postsSlice.js
const postsSlice = createSlice(/* omit slice code*/)

export const { postAdded, postUpdated, reactionAdded } = postsSlice.actions

export default postsSlice.reducer

export const selectAllPosts = state => state.posts

export const selectPostById = (state, postId) =>
state.posts.find(post => post.id === postId)

请注意,这些选择器函数的 state 参数是根 Redux 状态对象,因为它是我们直接在 useSelector 内部编写的内联匿名选择器的参数。

¥Note that the state parameter for these selector functions is the root Redux state object, as it was for the inlined anonymous selectors we wrote directly inside of useSelector.

然后我们可以在组件中使用它们:

¥We can then use them in the components:

features/posts/PostsList.js
// omit imports
import { selectAllPosts } from './postsSlice'

export const PostsList = () => {
const posts = useSelector(selectAllPosts)
// omit component contents
}
features/posts/SinglePostPage.js
// omit imports
import { selectPostById } from './postsSlice'

export const SinglePostPage = ({ match }) => {
const { postId } = match.params

const post = useSelector(state => selectPostById(state, postId))
// omit component logic
}
features/posts/EditPostForm.js
// omit imports
import { postUpdated, selectPostById } from './postsSlice'

export const EditPostForm = ({ match }) => {
const { postId } = match.params

const post = useSelector(state => selectPostById(state, postId))
// omit component logic
}

通过编写可重用选择器来封装数据查找通常是一个好主意。你还可以创建有助于提高性能的 "memoized" 选择器,我们将在本教程的后面部分介绍这一点。

¥It's often a good idea to encapsulate data lookups by writing reusable selectors. You can also create "memoized" selectors that can help improve performance, which we'll look at in a later part of this tutorial.

但是,就像任何抽象一样,你不应该随时随地都这样做。编写选择器意味着需要理解和维护更多的代码。不要觉得你需要为你状态的每个字段编写选择器。尝试从不使用任何选择器开始,然后当你发现自己在应用代码的许多部分中查找相同的值时添加一些选择器。

¥But, like any abstraction, it's not something you should do all the time, everywhere. Writing selectors means more code to understand and maintain. Don't feel like you need to write selectors for every single field of your state. Try starting without any selectors, and add some later when you find yourself looking up the same values in many parts of your application code.

请求的加载状态

¥Loading State for Requests

当我们进行 API 调用时,我们可以将其进度视为一个小型状态机,该状态机可以处于四种可能状态之一:

¥When we make an API call, we can view its progress as a small state machine that can be in one of four possible states:

  • 请求尚未开始

    ¥The request hasn't started yet

  • 请求正在进行中

    ¥The request is in progress

  • 请求成功,我们现在有了我们需要的数据

    ¥The request succeeded, and we now have the data we need

  • 请求失败,可能有错误消息

    ¥The request failed, and there's probably an error message

我们可以使用一些布尔值(例如 isLoading: true)来跟踪该信息,但最好将这些状态作为单个枚举值来跟踪。一个好的模式是有一个如下所示的状态部分(使用 TypeScript 类型表示法):

¥We could track that information using some booleans, like isLoading: true, but it's better to track these states as a single enum value. A good pattern for this is to have a state section that looks like this (using TypeScript type notation):

{
// Multiple possible status enum values
status: 'idle' | 'loading' | 'succeeded' | 'failed',
error: string | null
}

这些字段将与存储的任何实际数据一起存在。这些特定的字符串状态名称不是必需的 - 如果你愿意,可以随意使用其他名称,例如 'pending' 代替 'loading',或 'complete' 代替 'succeeded'

¥These fields would exist alongside whatever actual data is being stored. These specific string state names aren't required - feel free to use other names if you want, like 'pending' instead of 'loading', or 'complete' instead of 'succeeded'.

我们可以使用这些信息来决定在请求进行时在 UI 中显示什么内容,还可以在 reducer 中添加逻辑以防止诸如加载数据两次之类的情况。

¥We can use this information to decide what to show in our UI as the request progresses, and also add logic in our reducers to prevent cases like loading data twice.

让我们更新 postsSlice 以使用此模式来跟踪 "获取帖子" 请求的加载状态。我们将把我们的状态从帖子数组本身切换为看起来像 {posts, status, error}。我们还将从初始状态中删除旧的示例帖子条目。作为此更改的一部分,我们还需要将 state 作为数组的任何使用更改为 state.posts,因为该数组现在更深了一层:

¥Let's update our postsSlice to use this pattern to track loading state for a "fetch posts" request. We'll switch our state from being an array of posts by itself, to look like {posts, status, error}. We'll also remove the old sample post entries from our initial state. As part of this change, we also need to change any uses of state as an array to be state.posts instead, because the array is now one level deeper:

features/posts/postsSlice.js
import { createSlice, nanoid } from '@reduxjs/toolkit'

const initialState = {
posts: [],
status: 'idle',
error: null
}

const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
postAdded: {
reducer(state, action) {
state.posts.push(action.payload)
},
prepare(title, content, userId) {
// omit prepare logic
}
},
reactionAdded(state, action) {
const { postId, reaction } = action.payload
const existingPost = state.posts.find(post => post.id === postId)
if (existingPost) {
existingPost.reactions[reaction]++
}
},
postUpdated(state, action) {
const { id, title, content } = action.payload
const existingPost = state.posts.find(post => post.id === id)
if (existingPost) {
existingPost.title = title
existingPost.content = content
}
}
}
})

export const { postAdded, postUpdated, reactionAdded } = postsSlice.actions

export default postsSlice.reducer

export const selectAllPosts = state => state.posts.posts

export const selectPostById = (state, postId) =>
state.posts.posts.find(post => post.id === postId)

是的,这确实意味着我们现在有一个看起来像 state.posts.posts 的嵌套对象路径,这有点重复和愚蠢:)如果我们想避免这种情况,我们可以将嵌套数组名称更改为 itemsdata 或其他名称,但我们' 暂时保持原样。

¥Yes, this does mean that we now have a nested object path that looks like state.posts.posts, which is somewhat repetitive and silly :) We could change the nested array name to be items or data or something if we wanted to avoid that, but we'll leave it as-is for now.

使用 createAsyncThunk 获取数据

¥Fetching Data with createAsyncThunk

Redux Toolkit 的 createAsyncThunk API 会生成 thunk,自动为你分派这些 "开始/成功/失败" 操作。

¥Redux Toolkit's createAsyncThunk API generates thunks that automatically dispatch those "start/success/failure" actions for you.

让我们首先添加一个 thunk,它将进行 AJAX 调用来检索帖子列表。我们将从 src/api 文件夹导入 client 实用程序,并使用它向 '/fakeApi/posts' 发出请求。

¥Let's start by adding a thunk that will make an AJAX call to retrieve a list of posts. We'll import the client utility from the src/api folder, and use that to make a request to '/fakeApi/posts'.

features/posts/postsSlice
import { createSlice, nanoid, createAsyncThunk } from '@reduxjs/toolkit'
import { client } from '../../api/client'

const initialState = {
posts: [],
status: 'idle',
error: null
}

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

createAsyncThunk 接受两个参数:

¥createAsyncThunk accepts two arguments:

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

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

  • 一个 "有效负载创建者" 回调函数,应该返回包含一些数据的 Promise,或者返回带有错误的被拒绝的 Promise

    ¥A "payload creator" callback function that should return a Promise containing some data, or a rejected Promise with an error

有效负载创建者通常会进行某种 AJAX 调用,并且可以直接从 AJAX 调用返回 Promise,或者从 API 响应中提取一些数据并返回。我们通常使用 JS async/await 语法来编写它,这样我们就可以编写使用 Promise 的函数,同时使用标准 try/catch 逻辑而不是 somePromise.then() 链。

¥The payload creator will usually make an AJAX call of some kind, and can either return the Promise from the AJAX call directly, or extract some data from the API response and return that. We typically write this using the JS async/await syntax, which lets us write functions that use Promises while using standard try/catch logic instead of somePromise.then() chains.

在本例中,我们传入 'posts/fetchPosts' 作为操作类型前缀。我们的有效负载创建回调等待 API 调用返回响应。响应对象看起来像 {data: []},我们希望调度的 Redux 操作具有只是帖子数组的有效负载。因此,我们提取 response.data,并从回调中返回它。

¥In this case, we pass in 'posts/fetchPosts' as the action type prefix. Our payload creation callback waits for the API call to return a response. The response object looks like {data: []}, and we want our dispatched Redux action to have a payload that is just the array of posts. So, we extract response.data, and return that from the callback.

如果我们尝试调用 dispatch(fetchPosts())fetchPosts thunk 将首先调度 'posts/fetchPosts/pending' 的操作类型:

¥If we try calling dispatch(fetchPosts()), the fetchPosts thunk will first dispatch an action type of 'posts/fetchPosts/pending':

createAsyncThunk: posts pending action

我们可以在 reducer 中监听此操作并将请求状态标记为 'loading'

¥We can listen for this action in our reducer and mark the request status as 'loading'.

一旦 Promise 解析,fetchPosts thunk 就会获取我们从回调返回的 response.data 数组,并分派包含 posts 数组的 'posts/fetchPosts/fulfilled' 操作作为 action.payload

¥Once the Promise resolves, the fetchPosts thunk takes the response.data array we returned from the callback, and dispatches a 'posts/fetchPosts/fulfilled' action containing the posts array as action.payload:

createAsyncThunk: posts pending action

从组件中分派 thunk

¥Dispatching Thunks from Components

因此,让我们更新 <PostsList> 组件以实际自动为我们获取这些数据。

¥So, let's update our <PostsList> component to actually fetch this data automatically for us.

我们将 fetchPosts thunk 导入到组件中。与所有其他动作创建器一样,我们必须分派它,因此我们还需要添加 useDispatch 钩子。由于我们想要在 <PostsList> 挂载时获取此数据,因此我们需要导入 React useEffect 钩子:

¥We'll import the fetchPosts thunk into the component. Like all of our other action creators, we have to dispatch it, so we'll also need to add the useDispatch hook. Since we want to fetch this data when <PostsList> mounts, we need to import the React useEffect hook:

features/posts/PostsList.js
import React, { useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
// omit other imports
import { selectAllPosts, fetchPosts } from './postsSlice'

export const PostsList = () => {
const dispatch = useDispatch()
const posts = useSelector(selectAllPosts)

const postStatus = useSelector(state => state.posts.status)

useEffect(() => {
if (postStatus === 'idle') {
dispatch(fetchPosts())
}
}, [postStatus, dispatch])

// omit rendering logic
}

重要的是我们只尝试获取帖子列表一次。如果我们每次渲染 <PostsList> 组件时都执行此操作,或者由于在视图之间切换而重新创建组件,则最终可能会多次获取帖子。我们可以使用 posts.status 枚举来帮助决定是否需要实际开始获取,方法是将其选择到组件中,并且仅在状态为 'idle' 时才开始获取。

¥It's important that we only try to fetch the list of posts once. If we do it every time the <PostsList> component renders, or is re-created because we've switched between views, we might end up fetching the posts several times. We can use the posts.status enum to help decide if we need to actually start fetching, by selecting that into the component and only starting the fetch if the status is 'idle'.

reducer 和加载动作

¥Reducers and Loading Actions

接下来,我们需要在 reducer 中处理这两个操作。这需要更深入地了解我们一直在使用的 createSlice API。

¥Next up, we need to handle both these actions in our reducers. This requires a bit deeper look at the createSlice API we've been using.

我们已经看到 createSlice 将为我们在 reducers 字段中定义的每个 reducer 函数生成一个动作创建者,并且生成的动作类型包括切片的名称,例如:

¥We've already seen that createSlice will generate an action creator for every reducer function we define in the reducers field, and that the generated action types include the name of the slice, like:

console.log(
postUpdated({ id: '123', title: 'First Post', content: 'Some text here' })
)
/*
{
type: 'posts/postUpdated',
payload: {
id: '123',
title: 'First Post',
content: 'Some text here'
}
}
*/

但是,有时切片缩减器需要响应未定义为此切片的 reducers 字段的一部分的其他操作。我们可以使用切片 extraReducers 字段来代替。

¥However, there are times when a slice reducer needs to respond to other actions that weren't defined as part of this slice's reducers field. We can do that using the slice extraReducers field instead.

extraReducers 选项应该是一个接收名为 builder 的参数的函数。builder 对象提供了一些方法,让我们可以定义额外的 case 缩减器,这些缩减器将响应切片外部定义的操作而运行。我们将使用 builder.addCase(actionCreator, reducer) 来处理由异步 thunk 调度的每个操作。

¥The extraReducers option should be a function that receives a parameter called builder. The builder object provides methods that let us define additional case reducers that will run in response to actions defined outside of the slice. We'll use builder.addCase(actionCreator, reducer) to handle each of the actions dispatched by our async thunks.

Detailed Explanation: Adding Extra Reducers to Slices

extraReducers 中的 builder 对象提供了一些方法,让我们可以定义额外的 case 缩减器,这些缩减器将响应切片外部定义的操作而运行:

¥The builder object in extraReducers provides methods that let us define additional case reducers that will run in response to actions defined outside of the slice:

  • builder.addCase(actionCreator, reducer):定义一个 case 缩减器,用于处理基于 RTK 动作创建者或普通动作类型字符串的单个已知动作类型

    ¥builder.addCase(actionCreator, reducer): defines a case reducer that handles a single known action type based on either an RTK action creator or a plain action type string

  • builder.addMatcher(matcher, reducer):定义一个 case 缩减器,可以响应 matcher 函数返回 true 的任何操作而运行

    ¥builder.addMatcher(matcher, reducer): defines a case reducer that can run in response to any action where the matcher function returns true

  • builder.addDefaultCase(reducer):定义一个 case 缩减程序,如果没有为此操作执行其他 case 缩减程序,则该程序将运行。

    ¥builder.addDefaultCase(reducer): defines a case reducer that will run if no other case reducers were executed for this action.

你可以将它们链接在一起,例如 builder.addCase().addCase().addMatcher().addDefaultCase()。如果多个匹配器与该操作匹配,它们将按照定义的顺序运行。

¥You can chain these together, like builder.addCase().addCase().addMatcher().addDefaultCase(). If multiple matchers match the action, they will run in the order they were defined.

import { increment } from '../features/counter/counterSlice'

const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
// slice-specific reducers here
},
extraReducers: builder => {
builder
.addCase('counter/decrement', (state, action) => {})
.addCase(increment, (state, action) => {})
}
})

在这种情况下,我们需要监听 fetchPosts thunk 调度的 "pending" 和 "fulfilled" 操作类型。这些动作创建者附加到我们实际的 fetchPost 函数,我们可以将它们传递给 extraReducers 来监听这些动作:

¥In this case, we need to listen for the "pending" and "fulfilled" action types dispatched by our fetchPosts thunk. Those action creators are attached to our actual fetchPost function, and we can pass those to extraReducers to listen for those actions:

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

const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
// omit existing reducers here
},
extraReducers(builder) {
builder
.addCase(fetchPosts.pending, (state, action) => {
state.status = 'loading'
})
.addCase(fetchPosts.fulfilled, (state, action) => {
state.status = 'succeeded'
// Add any fetched posts to the array
state.posts = state.posts.concat(action.payload)
})
.addCase(fetchPosts.rejected, (state, action) => {
state.status = 'failed'
state.error = action.error.message
})
}
})

我们将根据我们返回的 Promise 处理可由 thunk 分派的所有三种操作类型:

¥We'll handle all three action types that could be dispatched by the thunk, based on the Promise we returned:

  • 当请求开始时,我们将 status 枚举设置为 'loading'

    ¥When the request starts, we'll set the status enum to 'loading'

  • 如果请求成功,我们将 status 标记为 'succeeded',并将获取的帖子添加到 state.posts

    ¥If the request succeeds, we mark the status as 'succeeded', and add the fetched posts to state.posts

  • 如果请求失败,我们会将 status 标记为 'failed',并将任何错误消息保存到状态中,以便我们可以显示它

    ¥If the request fails, we'll mark the status as 'failed', and save any error message into the state so we can display it

显示加载状态

¥Displaying Loading State

我们的 <PostsList> 组件已经在检查 Redux 中存储的帖子的任何更新,并在列表发生变化时重新渲染自身。因此,如果我们刷新页面,我们应该会在屏幕上看到来自假 API 的一组随机帖子:

¥Our <PostsList> component is already checking for any updates to the posts that are stored in Redux, and rerendering itself any time that list changes. So, if we refresh the page, we should see a random set of posts from our fake API show up on screen:

我们使用的假 API 会立即返回数据。然而,真正的 API 调用可能需要一些时间才能返回响应。在 UI 中显示某种 "正在加载..." 指示器通常是个好主意,这样用户就知道我们正在等待数据。

¥The fake API we're using returns data immediately. However, a real API call will probably take some time to return a response. It's usually a good idea to show some kind of "loading..." indicator in the UI so the user knows we're waiting for data.

我们可以更新 <PostsList> 以根据 state.posts.status 枚举显示不同的 UI:如果我们正在加载,则显示一个加载控件;如果失败,则显示一条错误消息;如果我们有数据,则显示实际的帖子列表。当我们这样做时,这可能是提取 <PostExcerpt> 组件来封装列表中一项的渲染的好时机。

¥We can update our <PostsList> to show a different bit of UI based on the state.posts.status enum: a spinner if we're loading, an error message if it failed, or the actual posts list if we have the data. While we're at it, this is probably a good time to extract a <PostExcerpt> component to encapsulate the rendering for one item in the list as well.

结果可能如下所示:

¥The result might look like this:

features/posts/PostsList.js
import { Spinner } from '../../components/Spinner'
import { PostAuthor } from './PostAuthor'
import { TimeAgo } from './TimeAgo'
import { ReactionButtons } from './ReactionButtons'
import { selectAllPosts, fetchPosts } from './postsSlice'

const PostExcerpt = ({ post }) => {
return (
<article className="post-excerpt">
<h3>{post.title}</h3>
<div>
<PostAuthor userId={post.user} />
<TimeAgo timestamp={post.date} />
</div>
<p className="post-content">{post.content.substring(0, 100)}</p>

<ReactionButtons post={post} />
<Link to={`/posts/${post.id}`} className="button muted-button">
View Post
</Link>
</article>
)
}

export const PostsList = () => {
const dispatch = useDispatch()
const posts = useSelector(selectAllPosts)

const postStatus = useSelector(state => state.posts.status)
const error = useSelector(state => state.posts.error)

useEffect(() => {
if (postStatus === 'idle') {
dispatch(fetchPosts())
}
}, [postStatus, dispatch])

let content

if (postStatus === 'loading') {
content = <Spinner text="Loading..." />
} else if (postStatus === 'succeeded') {
// Sort posts in reverse chronological order by datetime string
const orderedPosts = posts
.slice()
.sort((a, b) => b.date.localeCompare(a.date))

content = orderedPosts.map(post => (
<PostExcerpt key={post.id} post={post} />
))
} else if (postStatus === 'failed') {
content = <div>{error}</div>
}

return (
<section className="posts-list">
<h2>Posts</h2>
{content}
</section>
)
}

你可能会注意到 API 调用需要一段时间才能完成,并且加载加载控件会在屏幕上停留几秒钟。我们的模拟 API 服务器配置为向所有响应添加 2 秒的延迟,特别是为了帮助可视化可见加载旋转器的时间。如果你想改变这个行为,你可以打开 api/server.js,并改变这一行:

¥You might notice that the API calls are taking a while to complete, and that the loading spinner is staying on screen for a couple seconds. Our mock API server is configured to add a 2-second delay to all responses, specifically to help visualize times when there's a loading spinner visible. If you want to change this behavior, you can open up api/server.js, and alter this line:

api/server.js
// Add an extra delay to all endpoints, so loading spinners show up.
const ARTIFICIAL_DELAY_MS = 2000

如果你希望 API 调用更快完成,请随时打开和关闭它。

¥Feel free to turn that on and off as we go if you want the API calls to complete faster.

加载用户

¥Loading Users

我们现在正在获取并显示我们的帖子列表。但是,如果我们看一下帖子,就会发现一个问题:他们现在都说 "作者未知" 作为作者:

¥We're now fetching and displaying our list of posts. But, if we look at the posts, there's a problem: they all now say "Unknown author" as the authors:

Unknown post authors

这是因为帖子条目是由虚假 API 服务器随机生成的,每次我们重新加载页面时,该服务器也会随机生成一组虚假用户。我们需要更新用户切片以在应用启动时获取这些用户。

¥This is because the post entries are being randomly generated by the fake API server, which also randomly generates a set of fake users every time we reload the page. We need to update our users slice to fetch those users when the application starts.

和上次一样,我们将创建另一个异步 thunk 以从 API 获取用户并返回它们,然后处理 extraReducers 切片字段中的 fulfilled 操作。我们现在暂时不用担心加载状态:

¥Like last time, we'll create another async thunk to get the users from the API and return them, then handle the fulfilled action in the extraReducers slice field. We'll skip worrying about loading state for now:

features/users/usersSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import { client } from '../../api/client'

const initialState = []

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

const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {},
extraReducers(builder) {
builder.addCase(fetchUsers.fulfilled, (state, action) => {
return action.payload
})
}
})

export default usersSlice.reducer

你可能已经注意到,这次 case 缩减器根本不使用 state 变量。相反,我们直接返回 action.payload。Immer 让我们可以通过两种方式更新状态:要么改变现有的状态值,要么返回新的结果。如果我们返回一个新值,它将用我们返回的值完全替换现有状态。(请注意,如果你想手动返回新值,则需要你编写可能需要的任何不可变更新逻辑。)

¥You may have noticed that this time the case reducer isn't using the state variable at all. Instead, we're returning the action.payload directly. Immer lets us update state in two ways: either mutating the existing state value, or returning a new result. If we return a new value, that will replace the existing state completely with whatever we return. (Note that if you want to manually return a new value, it's up to you to write any immutable update logic that might be needed.)

在这种情况下,初始状态是一个空数组,我们可能可以执行 state.push(...action.payload) 来改变它。但是,在我们的例子中,我们确实希望用服务器返回的任何内容替换用户列表,这可以避免意外复制状态中的用户列表的任何机会。

¥In this case, the initial state was an empty array, and we probably could have done state.push(...action.payload) to mutate it. But, in our case we really want to replace the list of users with whatever the server returned, and this avoids any chance of accidentally duplicating the list of users in state.

信息

要了解有关 Immer 状态更新如何工作的更多信息,请参阅 RTK 文档中的 "使用 Immer 编写 reducer" 指南

¥To learn more about how state updates with Immer work, see the "Writing Reducers with Immer" guide in the RTK docs.

我们只需要获取用户列表一次,并且我们希望在应用启动时正确执行此操作。我们可以在 index.js 文件中执行此操作,并直接分派 fetchUsers thunk,因为我们有 store

¥We only need to fetch the list of users once, and we want to do it right when the application starts. We can do that in our index.js file, and directly dispatch the fetchUsers thunk because we have the store right there:

index.js
// omit other imports

import store from './app/store'
import { fetchUsers } from './features/users/usersSlice'

import { worker } from './api/server'

async function main() {
// Start our mock API server
await worker.start({ onUnhandledRequest: 'bypass' })

store.dispatch(fetchUsers())

ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
)
}
main()

现在,每个帖子都应该再次显示用户名,并且我们还应该在 <AddPostForm> 的 "作者" 下拉列表中显示相同的用户列表。

¥Now, each of the posts should be showing a username again, and we should also have that same list of users shown in the "Author" dropdown in our <AddPostForm>.

添加新帖子

¥Adding New Posts

本节我们还有一个步骤。当我们从 <AddPostForm> 添加新帖子时,该帖子只会添加到我们应用内的 Redux 存储中。我们实际上需要进行一个 API 调用,在我们的假 API 服务器中创建新的帖子条目,因此它是 "saved"。(由于这是一个假 API,因此如果我们重新加载页面,新帖子将不会保留,但如果我们有一个真正的后端服务器,那么下次重新加载时它将可用。)

¥We have one more step for this section. When we add a new post from the <AddPostForm>, that post is only getting added to the Redux store inside our app. We need to actually make an API call that will create the new post entry in our fake API server instead, so that it's "saved". (Since this is a fake API, the new post won't persist if we reload the page, but if we had a real backend server it would be available next time we reload.)

使用 Thunk 发送数据

¥Sending Data with Thunks

我们可以使用 createAsyncThunk 来帮助发送数据,而不仅仅是获取数据。我们将创建一个 thunk,它接受 <AddPostForm> 中的值作为参数,并对假 API 进行 HTTP POST 调用以保存数据。

¥We can use createAsyncThunk to help with sending data, not just fetching it. We'll create a thunk that accepts the values from our <AddPostForm> as an argument, and makes an HTTP POST call to the fake API to save the data.

在此过程中,我们将更改在 reducer 中使用新 post 对象的方式。目前,我们的 postsSlice 正在 prepare 回调中为 postAdded 创建一个新的帖子对象,并为该帖子生成一个新的唯一 ID。在大多数将数据保存到服务器的应用中,服务器将负责生成唯一 ID 并填写任何额外字段,并且通常会在其响应中返回完整的数据。因此,我们可以向服务器发送一个像 { title, content, user: userId } 这样的请求体,然后获取它发回的完整的 post 对象并将其添加到我们的 postsSlice 状态中。

¥In the process, we're going to change how we work with the new post object in our reducers. Currently, our postsSlice is creating a new post object in the prepare callback for postAdded, and generating a new unique ID for that post. In most apps that save data to a server, the server will take care of generating unique IDs and filling out any extra fields, and will usually return the completed data in its response. So, we can send a request body like { title, content, user: userId } to the server, and then take the complete post object it sends back and add it to our postsSlice state.

features/posts/postsSlice.js
export const addNewPost = createAsyncThunk(
'posts/addNewPost',
// The payload creator receives the partial `{title, content, user}` object
async initialPost => {
// We send the initial data to the fake API server
const response = await client.post('/fakeApi/posts', initialPost)
// The response includes the complete post object, including unique ID
return response.data
}
)

const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
// The existing `postAdded` reducer and prepare callback were deleted
reactionAdded(state, action) {}, // omit logic
postUpdated(state, action) {} // omit logic
},
extraReducers(builder) {
// omit posts loading reducers
builder.addCase(addNewPost.fulfilled, (state, action) => {
// We can directly add the new post object to our posts array
state.posts.push(action.payload)
})
}
})

检查组件中的 Thunk 结果

¥Checking Thunk Results in Components

最后,我们将更新 <AddPostForm> 以调度 addNewPost thunk,而不是旧的 postAdded 操作。由于这是对服务器的另一个 API 调用,因此需要一些时间并且可能会失败。addNewPost() thunk 会自动将其 pending/fulfilled/rejected 操作分派到我们已经在处理的 Redux 存储。如果愿意,我们可以使用第二个加载枚举来跟踪 postsSlice 中的请求状态,但对于此示例,让我们将加载状态跟踪限制为组件。

¥Finally, we'll update <AddPostForm> to dispatch the addNewPost thunk instead of the old postAdded action. Since this is another API call to the server, it will take some time and could fail. The addNewPost() thunk will automatically dispatch its pending/fulfilled/rejected actions to the Redux store, which we're already handling. We could track the request status in postsSlice using a second loading enum if we wanted to, but for this example let's keep the loading state tracking limited to the component.

如果我们在等待请求时至少可以禁用 "保存帖子" 按钮,那就太好了,这样用户就不会意外地尝试保存帖子两次。如果请求失败,我们可能还想在表单中显示错误消息,或者只是将其记录到控制台。

¥It would be good if we can at least disable the "Save Post" button while we're waiting for the request, so the user can't accidentally try to save a post twice. If the request fails, we might also want to show an error message here in the form, or perhaps just log it to the console.

我们可以让组件逻辑等待 async thunk 完成,并在完成后检查结果:

¥We can have our component logic wait for the async thunk to finish, and check the result when it's done:

features/posts/AddPostForm.js
import React, { useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'

import { addNewPost } from './postsSlice'

export const AddPostForm = () => {
const [title, setTitle] = useState('')
const [content, setContent] = useState('')
const [userId, setUserId] = useState('')
const [addRequestStatus, setAddRequestStatus] = useState('idle')

// omit useSelectors and change handlers

const canSave =
[title, content, userId].every(Boolean) && addRequestStatus === 'idle'

const onSavePostClicked = async () => {
if (canSave) {
try {
setAddRequestStatus('pending')
await dispatch(addNewPost({ title, content, user: userId })).unwrap()
setTitle('')
setContent('')
setUserId('')
} catch (err) {
console.error('Failed to save the post: ', err)
} finally {
setAddRequestStatus('idle')
}
}
}

// omit rendering logic
}

我们可以添加一个加载状态枚举字段作为 React useState 钩子,类似于我们在 postsSlice 中跟踪加载状态以获取帖子的方式。在这种情况下,我们只想知道请求是否正在进行。

¥We can add a loading status enum field as a React useState hook, similar to how we're tracking loading state in postsSlice for fetching posts. In this case, we just want to know if the request is in progress or not.

当我们调用 dispatch(addNewPost()) 时,异步 thunk 从 dispatch 返回 Promise。我们可以在这里确保 await 知道 thunk 何时完成其请求。但是,我们还不知道该请求是成功还是失败。

¥When we call dispatch(addNewPost()), the async thunk returns a Promise from dispatch. We can await that promise here to know when the thunk has finished its request. But, we don't yet know if that request succeeded or failed.

createAsyncThunk 在内部处理任何错误,因此我们在日志中看不到任何有关 "拒绝的 Promise" 的消息。然后它返回它调度的最终操作:如果成功则执行 fulfilled 操作,如果失败则执行 rejected 操作。

¥createAsyncThunk handles any errors internally, so that we don't see any messages about "rejected Promises" in our logs. It then returns the final action it dispatched: either the fulfilled action if it succeeded, or the rejected action if it failed.

但是,通常希望编写逻辑来查看所发出的实际请求的成功或失败。Redux Toolkit 将 .unwrap() 函数添加到返回的 Promise 中,这将返回一个新的 Promise,该 Promise 要么具有 fulfilled 操作的实际 action.payload 值,要么如果是 rejected 操作则抛出错误。这让我们可以使用正常的 try/catch 逻辑来处理组件中的成功和失败。因此,如果帖子创建成功,我们将清除输入字段以重置表单,如果失败,我们将把错误记录到控制台。

¥However, it's common to want to write logic that looks at the success or failure of the actual request that was made. Redux Toolkit adds a .unwrap() function to the returned Promise, which will return a new Promise that either has the actual action.payload value from a fulfilled action, or throws an error if it's the rejected action. This lets us handle success and failure in the component using normal try/catch logic. So, we'll clear out the input fields to reset the form if the post was successfully created, and log the error to the console if it failed.

如果你想查看 addNewPost API 调用失败时会发生什么,请尝试创建一个新帖子,其中 "内容" 字段仅包含单词 "error"(不带引号)。服务器将看到这一点并发回失败的响应,因此你应该看到一条记录到控制台的消息。

¥If you want to see what happens when the addNewPost API call fails, try creating a new post where the "Content" field only has the word "error" (without quotes). The server will see that and send back a failed response, so you should see a message logged to the console.

你学到了什么

¥What You've Learned

异步逻辑和数据获取始终是一个复杂的主题。正如你所看到的,Redux Toolkit 包含一些工具来自动执行典型的 Redux 数据获取模式。

¥Async logic and data fetching are always a complex topic. As you've seen, Redux Toolkit includes some tools to automate the typical Redux data fetching patterns.

现在我们从那个假 API 获取数据,我们的应用如下所示:

¥Here's what our app looks like now that we're fetching data from that fake API:

提醒一下,这是我们在本节中介绍的内容:

¥As a reminder, here's what we covered in this section:

概括
  • 你可以编写可重用的 "selector" 函数来封装从 Redux 状态读取值

    ¥You can write reusable "selector" functions to encapsulate reading values from the Redux state

    • 选择器是获取 Redux state 作为参数并返回一些数据的函数

      ¥Selectors are functions that get the Redux state as an argument, and return some data

  • Redux 使用名为 "中间件" 的插件来启用异步逻辑

    ¥Redux uses plugins called "middleware" to enable async logic

    • 标准异步中间件称为 redux-thunk,它包含在 Redux Toolkit 中

      ¥The standard async middleware is called redux-thunk, which is included in Redux Toolkit

    • Thunk 函数接收 dispatchgetState 作为参数,并可以将它们用作异步逻辑的一部分

      ¥Thunk functions receive dispatch and getState as arguments, and can use those as part of async logic

  • 你可以调度其他操作来帮助跟踪 API 调用的加载状态

    ¥You can dispatch additional actions to help track the loading status of an API call

    • 典型的模式是在调用之前调度 "pending" 操作,然后调度包含数据的 "success" 或包含错误的 "failure" 操作

      ¥The typical pattern is dispatching a "pending" action before the call, then either a "success" containing the data or a "failure" action containing the error

    • 加载状态通常应存储为枚举,例如 'idle' | 'loading' | 'succeeded' | 'failed'

      ¥Loading state should usually be stored as an enum, like 'idle' | 'loading' | 'succeeded' | 'failed'

  • Redux Toolkit 有一个 createAsyncThunk API 可以为你分派这些操作

    ¥Redux Toolkit has a createAsyncThunk API that dispatches these actions for you

    • createAsyncThunk 接受应返回 Promise 的 "有效负载创建者" 回调,并自动生成 pending/fulfilled/rejected 操作类型

      ¥createAsyncThunk accepts a "payload creator" callback that should return a Promise, and generates pending/fulfilled/rejected action types automatically

    • 生成的操作创建者(例如 fetchPosts)根据你返回的 Promise 调度这些操作

      ¥Generated action creators like fetchPosts dispatch those actions based on the Promise you return

    • 你可以使用 extraReducers 字段监听 createSlice 中的这些操作类型,并根据这些操作更新 reducer 中的状态。

      ¥You can listen for these action types in createSlice using the extraReducers field, and update the state in reducers based on those actions.

    • 动作创建器可用于自动填充 extraReducers 对象的键,以便切片知道要监听哪些动作。

      ¥Action creators can be used to automatically fill in the keys of the extraReducers object so the slice knows what actions to listen for.

    • thunks 可以返回 promise。具体来说,对于 createAsyncThunk,你可以 await dispatch(someThunk()).unwrap() 在组件级别处理请求成功或失败。

      ¥Thunks can return promises. For createAsyncThunk specifically, you can await dispatch(someThunk()).unwrap() to handle the request success or failure at the component level.

下一步是什么?

¥What's Next?

我们还有一组主题来涵盖核心 Redux Toolkit API 和使用模式。在 第 6 部分:性能和标准化数据 中,我们将了解 Redux 的使用如何影响 React 性能,以及我们可以优化应用以提高性能的一些方法。

¥We've got one more set of topics to cover the core Redux Toolkit APIs and usage patterns. In Part 6: Performance and Normalizing Data, we'll look at how Redux usage affects React performance, and some ways we can optimize our application for improved performance.