Skip to main content

Redux 要点,第 7 部分:RTK 查询基础知识

你将学到什么
  • RTK 查询如何简化 Redux 应用的数据获取

    ¥How RTK Query simplifies data fetching for Redux apps

  • 如何设置 RTK 查询

    ¥How to set up RTK Query

  • 如何使用 RTK 查询进行基本数据获取和更新请求

    ¥How to use RTK Query for basic data fetching and update requests

先决条件
  • 完成本教程前面的部分以了解 Redux Toolkit 使用模式

    ¥Completion of the previous sections of this tutorial to understand Redux Toolkit usage patterns

更喜欢视频课程?

如果你更喜欢视频课程,你可以 在 Egghead 免费观看 RTK 查询创建者 Lenz Weber-Tronic 的 RTK 查询视频课程 或在此处观看第一课:

¥If you prefer a video course, you can watch this RTK Query video course by Lenz Weber-Tronic, the creator of RTK Query, for free at Egghead or take a look at the first lesson right here:

介绍

¥Introduction

第 5 部分:异步逻辑和数据获取第 6 部分:性能和标准化 中,我们看到了使用 Redux 进行数据获取和缓存的标准模式。这些模式包括使用异步 thunk 来获取数据、使用结果调度操作、管理存储中的请求加载状态以及规范化缓存数据以更轻松地按 ID 查找和更新各个项目。

¥in Part 5: Async Logic and Data Fetching and Part 6: Performance and Normalization, we saw the standard patterns used for data fetching and caching with Redux. Those patterns include using async thunks to fetch data, dispatching actions with the results, managing request loading state in the store, and normalizing the cached data to enable easier lookups and updates of individual items by ID.

在本节中,我们将了解如何使用 RTK Query(一种专为 Redux 应用设计的数据获取和缓存解决方案),并了解它如何简化获取数据并在我们的组件中使用数据的过程。

¥In this section, we'll look at how to use RTK Query, a data fetching and caching solution designed for Redux applications, and see how it simplifies the process of fetching data and using it in our components.

RTK 查询概述

¥RTK Query Overview

RTK Query 是一个强大的数据获取和缓存工具。它旨在简化在 Web 应用中加载数据的常见情况,无需你自己手动编写数据获取和缓存逻辑。

¥RTK Query is a powerful data fetching and caching tool. It is designed to simplify common cases for loading data in a web application, eliminating the need to hand-write data fetching & caching logic yourself.

RTK 查询是 Redux Toolkit 包中包含的可选插件,其功能构建在 Redux Toolkit 中的其他 API 之上。

¥RTK Query is an optional addon included in the Redux Toolkit package, and its functionality is built on top of the other APIs in Redux Toolkit.

动机

¥Motivation

Web 应用通常需要从服务器获取数据才能显示。他们通常还需要更新该数据,将这些更新发送到服务器,并使客户端上的缓存数据与服务器上的数据保持同步。由于需要实现当今应用中使用的其他行为,这变得更加复杂:

¥Web applications normally need to fetch data from a server in order to display it. They also usually need to make updates to that data, send those updates to the server, and keep the cached data on the client in sync with the data on the server. This is made more complicated by the need to implement other behaviors used in today's applications:

  • 跟踪加载状态以显示 UI 旋转器

    ¥Tracking loading state in order to show UI spinners

  • 避免对相同数据的重复请求

    ¥Avoiding duplicate requests for the same data

  • 乐观的更新让 UI 感觉更快

    ¥Optimistic updates to make the UI feel faster

  • 在用户与 UI 交互时管理缓存生命周期

    ¥Managing cache lifetimes as the user interacts with the UI

我们已经了解了如何使用 Redux Toolkit 实现这些行为。

¥We've already seen how we can implement these behaviors using Redux Toolkit.

然而,历史上 Redux 从未包含任何内置内容来帮助完全解决这些用例。即使我们将 createAsyncThunkcreateSlice 一起使用,仍然需要大量的手动工作来发出请求和管理加载状态。我们必须创建异步 thunk,发出实际请求,从响应中提取相关字段,添加加载状态字段,在 extraReducers 中添加处理程序来处理 pending/fulfilled/rejected 情况,并实际编写正确的状态更新。

¥However, historically Redux has never included anything built in to help completely solve these use cases. Even when we use createAsyncThunk together with createSlice, there's still a fair amount of manual work involved in making requests and managing loading state. We have to create the async thunk, make the actual request, pull relevant fields out of the response, add loading state fields, add handlers in extraReducers to handle the pending/fulfilled/rejected cases, and actually write the proper state updates.

在过去的几年里,React 社区逐渐意识到 "数据获取和缓存" 确实是与 "状态管理" 不同的一组关注点。虽然你可以使用 Redux 等状态管理库来缓存数据,但用例差异很大,值得使用专为数据获取用例构建的工具。

¥Over the last couple years, the React community has come to realize that "data fetching and caching" is really a different set of concerns than "state management". While you can use a state management library like Redux to cache data, the use cases are different enough that it's worth using tools that are purpose-built for the data fetching use case.

RTK Query 从其他率先推出数据获取解决方案的工具(例如 Apollo Client、React Query、Urql 和 SWR)中汲取灵感,但在其 API 设计中添加了独特的方法:

¥RTK Query takes inspiration from other tools that have pioneered solutions for data fetching, like Apollo Client, React Query, Urql, and SWR, but adds a unique approach to its API design:

  • 数据获取和缓存逻辑构建在 Redux Toolkit 的 createSlicecreateAsyncThunk API 之上

    ¥The data fetching and caching logic is built on top of Redux Toolkit's createSlice and createAsyncThunk APIs

  • 由于 Redux Toolkit 与 UI 无关,因此 RTK Query 的功能可与任何 UI 层一起使用

    ¥Because Redux Toolkit is UI-agnostic, RTK Query's functionality can be used with any UI layer

  • API 端点是提前定义的,包括如何从参数生成查询参数和转换响应以进行缓存

    ¥API endpoints are defined ahead of time, including how to generate query parameters from arguments and transform responses for caching

  • RTK Query 还可以生成 React hooks,封装整个数据获取过程,为组件提供 dataisFetching 字段,并在组件挂载和卸载时管理缓存数据的生命周期

    ¥RTK Query can also generate React hooks that encapsulate the entire data fetching process, provide data and isFetching fields to components, and manage the lifetime of cached data as components mount and unmount

  • RTK 查询提供 "缓存条目生命周期" 选项,支持在获取初始数据后通过 websocket 消息进行流式缓存更新等用例

    ¥RTK Query provides "cache entry lifecycle" options that enable use cases like streaming cache updates via websocket messages after fetching the initial data

  • 我们有从 OpenAPI 和 GraphQL 模式生成 API 切片代码的早期工作示例

    ¥We have early working examples of code generation of API slices from OpenAPI and GraphQL schemas

  • 最后,RTK Query 完全用 TypeScript 编写,旨在提供出色的 TS 使用体验

    ¥Finally, RTK Query is completely written in TypeScript, and is designed to provide an excellent TS usage experience

包含什么

¥What's included

蜜蜂

¥APIs

RTK 查询包含在核心 Redux Toolkit 包的安装中。它可以通过以下两个入口点之一获得:

¥RTK Query is included within the installation of the core Redux Toolkit package. It is available via either of the two entry points below:

import { createApi } from '@reduxjs/toolkit/query'

/* React-specific entry point that automatically generates
hooks corresponding to the defined endpoints */
import { createApi } from '@reduxjs/toolkit/query/react'

RTK 查询主要由两个 API 组成:

¥RTK Query primarily consists of two APIs:

  • createApi():RTK 查询功能的核心。它允许你定义一组端点,描述如何从一系列端点检索数据,包括配置如何获取和转换该数据。在大多数情况下,你应该为每个应用使用一次,根据经验,使用 "每个基本 URL 一个 API 切片"。

    ¥createApi(): The core of RTK Query's functionality. It allows you to define a set of endpoints describe how to retrieve data from a series of endpoints, including configuration of how to fetch and transform that data. In most cases, you should use this once per app, with "one API slice per base URL" as a rule of thumb.

  • fetchBaseQuery():围绕 fetch 的小封装,旨在简化请求。旨在作为推荐的 baseQuery 应用于 createApi 中,供广大用户使用。

    ¥fetchBaseQuery(): A small wrapper around fetch that aims to simplify requests. Intended as the recommended baseQuery to be used in createApi for the majority of users.

打包尺寸

¥Bundle Size

RTK 查询会向你的应用的打包包大小添加固定的一次性金额。由于 RTK Query 构建在 Redux Toolkit 和 React-Redux 之上,因此添加的大小会根据你是否已在应用中使用这些内容而有所不同。估计的最小+gzip 包大小为:

¥RTK Query adds a fixed one-time amount to your app's bundle size. Since RTK Query builds on top of Redux Toolkit and React-Redux, the added size varies depending on whether you are already using those in your app. The estimated min+gzip bundle sizes are:

  • 如果你已经使用 RTK:RTK 查询约 9kb,钩子约 2kb。

    ¥If you are using RTK already: ~9kb for RTK Query and ~2kb for the hooks.

  • 如果你尚未使用 RTK:

    ¥If you are not using RTK already:

    • 没有反应:RTK+依赖+RTK 查询 17 kB

      ¥Without React: 17 kB for RTK+dependencies+RTK Query

    • 使用反应:19kB + React-Redux,这是对等依赖

      ¥With React: 19kB + React-Redux, which is a peer dependency

添加额外的端点定义只会根据 endpoints 定义内的实际代码增加大小,通常只有几个字节。

¥Adding additional endpoint definitions should only increase size based on the actual code inside the endpoints definitions, which will typically be just a few bytes.

RTK 查询中包含的功能可以快速弥补增加的打包包大小,并且消除手写数据获取逻辑对于大多数有意义的应用来说应该是大小上的净改进。

¥The functionality included in RTK Query quickly pays for the added bundle size, and the elimination of hand-written data fetching logic should be a net improvement in size for most meaningful applications.

RTK 查询缓存的思考

¥Thinking in RTK Query Caching

Redux 始终强调可预测性和显式行为。Redux 中不涉及 "magic" - 你应该能够理解应用中发生的情况,因为所有 Redux 逻辑都遵循相同的基本模式:通过 reducer 分派操作和更新状态。这确实意味着有时你必须编写更多代码才能使事情发生,但权衡是应该非常清楚数据流和行为是什么。

¥Redux has always had an emphasis on predictability and explicit behavior. There's no "magic" involved in Redux - you should be able to understand what's happening in the application because all Redux logic follows the same basic patterns of dispatching actions and updating state via reducers. This does mean that sometimes you have to write more code to make things happen, but the tradeoff is that should be very clear what the data flow and behavior is.

Redux Toolkit 核心 API 不会更改 Redux 应用中的任何基本数据流。你仍然在分派操作并编写化简器,只是比手动编写所有逻辑所需的代码更少。RTK 查询也是同样的方法。这是一个额外的抽象级别,但在内部它仍然执行与我们已经看到的管理异步请求及其响应完全相同的步骤。

¥The Redux Toolkit core APIs do not change any of the basic data flow in a Redux app You're still dispatching actions and writing reducers, just with less code than writing all of that logic by hand. RTK Query is the same way. It's an additional level of abstraction, but internally it's still doing the exact same steps we've already seen for managing async requests and their responses.

然而,当你使用 RTK 查询时,思维方式就会发生转变。我们不再考虑 "管理状态" 本身。相反,我们现在考虑的是 "管理缓存数据"。我们现在不再尝试自己编写 reducer,而是专注于定义 "这些数据从哪里来?"、"应如何发送此更新?"、"什么时候应该重新获取缓存的数据?" 和 "缓存数据应该如何更新?"。如何获取、存储和检索数据成为我们不再需要担心的实现细节。

¥However, when you use RTK Query, there is a mindset shift that happens. We're no longer thinking about "managing state" per se. Instead, we now think about "managing cached data". Rather than trying to write reducers ourselves, we're now going to focus on defining "where is this data coming from?", "how should this update be sent?", "when should this cached data be re-fetched?", and "how should the cached data be updated?". How that data gets fetched, stored, and retrieved becomes implementation details we no longer have to worry about.

随着我们的继续,我们将看到这种思维方式的转变如何应用。

¥We'll see how this mindset shift applies as we continue.

设置 RTK 查询

¥Setting Up RTK Query

我们的示例应用已经可以运行,但现在是时候将所有异步逻辑迁移到使用 RTK 查询了。在我们阅读过程中,我们将了解如何使用 RTK 查询的所有主要功能,以及如何将 createAsyncThunkcreateSlice 的现有用途迁移到使用 RTK 查询 API。

¥Our example application already works, but now it's time to migrate all of the async logic over to use RTK Query. As we go through, we'll see how to use all the major features of RTK Query, as well as how to migrate existing uses of createAsyncThunk and createSlice over to use the RTK Query APIs.

定义 API 切片

¥Defining an API Slice

之前,我们为每种不同的数据类型(例如帖子、用户和通知)定义了单独的 "切片"。每个切片都有自己的化简器,定义自己的操作和 thunk,并单独缓存该数据类型的条目。

¥Previously, we've defined separate "slices" for each of our different data types like Posts, Users, and Notifications. Each slice had its own reducer, defined its own actions and thunks, and cached the entries for that data type separately.

通过 RTK 查询,管理缓存数据的逻辑被集中到每个应用的单个 "API 切片" 中。就像每个应用有一个 Redux 存储一样,我们现在有一个用于所有缓存数据的切片。

¥With RTK Query, the logic for managing cached data is centralized into a single "API slice" per application. In much the same way that you have a single Redux store per app, we now have a single slice for all our cached data.

我们首先定义一个新的 apiSlice.js 文件。由于这并不特定于我们已经编写的任何其他 "features",因此我们将添加一个新的 features/api/ 文件夹并将 apiSlice.js 放入其中。让我们填写 API 切片文件,然后分解里面的代码看看它在做什么:

¥We'll start by defining a new apiSlice.js file. Since this isn't specific to any of the other "features" we've already written, we'll add a new features/api/ folder and put apiSlice.js in there. Let's fill out the API slice file, and then break down the code inside to see what it's doing:

features/api/apiSlice.js
// Import the RTK Query methods from the React-specific entry point
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

// Define our single API slice object
export const apiSlice = createApi({
// The cache reducer expects to be added at `state.api` (already default - this is optional)
reducerPath: 'api',
// All of our requests will have URLs starting with '/fakeApi'
baseQuery: fetchBaseQuery({ baseUrl: '/fakeApi' }),
// The "endpoints" represent operations and requests for this server
endpoints: builder => ({
// The `getPosts` endpoint is a "query" operation that returns data
getPosts: builder.query({
// The URL for the request is '/fakeApi/posts'
query: () => '/posts'
})
})
})

// Export the auto-generated hook for the `getPosts` query endpoint
export const { useGetPostsQuery } = apiSlice

RTK 查询的功能基于一种称为 createApi 的方法。到目前为止,我们看到的所有 Redux Toolkit API 都是与 UI 无关的,并且可以与任何 UI 层一起使用。RTK 查询核心逻辑是一样的。然而,RTK Query 还包括 createApi 的 React 特定版本,由于我们同时使用 RTK 和 React,因此我们需要使用它来利用 RTK 的 React 集成。所以,我们专门从 '@reduxjs/toolkit/query/react' 导入。

¥RTK Query's functionality is based on a single method, called createApi. All of the Redux Toolkit APIs we've seen so far are UI-agnostic, and could be used with any UI layer. The RTK Query core logic is the same way. However, RTK Query also includes a React-specific version of createApi, and since we're using RTK and React together, we need to use that to take advantage of RTK's React integration. So, we import from '@reduxjs/toolkit/query/react' specifically.

提示

你的应用中预计只有一个 createApi 调用。这一 API 切片应包含与同一基本 URL 通信的所有端点定义。例如,端点 /api/posts/api/users 都从同一服务器获取数据,因此它们将进入同一 API 切片。如果你的应用确实从多个服务器获取数据,你可以在每个端点中指定完整的 URL,或者在必要时为每个服务器创建单独的 API 切片。

¥Your application is expected to have only one createApi call in it. This one API slice should contain all endpoint definitions that talk to the same base URL. For example, endpoints /api/posts and /api/users are both fetching data from the same server, so they would go in the same API slice. If your app does fetch data from multiple servers, you can either specify full URLs in each endpoint, or if necessary create separate API slices for each server.

端点通常直接在 createApi 调用内定义。如果你希望在多个文件之间拆分端点,请参阅文档的 第 8 部分中的 "注入端点" 部分 部分!

¥Endpoints are normally defined directly inside the createApi call. If you're looking to split up your endpoints between multiple files, see the "Injecting Endpoints" section in Part 8 section of the docs!

API 切片参数

¥API Slice Parameters

当我们调用 createApi 时,有两个字段是必填的:

¥When we call createApi, there are two fields that are required:

  • baseQuery:知道如何从服务器获取数据的函数。RTK 查询包括 fetchBaseQuery,它是标准 fetch() 函数的一个小封装器,用于处理请求和响应的典型处理。当我们创建 fetchBaseQuery 实例时,我们可以传入所有未来请求的基本 URL,以及覆盖行为,例如修改请求标头。

    ¥baseQuery: a function that knows how to fetch data from the server. RTK Query includes fetchBaseQuery, a small wrapper around the standard fetch() function that handles typical processing of requests and responses. When we create a fetchBaseQuery instance, we can pass in the base URL of all future requests, as well as override behavior such as modifying request headers.

  • endpoints:我们为与该服务器交互而定义的一组操作。端点可以是返回数据进行缓存的查询,也可以是向服务器发送更新的突变。端点是使用回调函数定义的,该函数接受 builder 参数并返回一个包含使用 builder.query()builder.mutation() 创建的端点定义的对象。

    ¥endpoints: a set of operations that we've defined for interacting with this server. Endpoints can be queries, which return data for caching, or mutations, which send an update to the server. The endpoints are defined using a callback function that accepts a builder parameter and returns an object containing endpoint definitions created with builder.query() and builder.mutation().

createApi 还接受 reducerPath 字段,该字段定义生成的 reducer 的预期顶层状态切片字段。对于我们的其他切片,例如 postsSlice,不能保证它将用于更新 state.posts - 我们可以将 reducer 附加到根状态的任何位置,例如 someOtherField: postsReducer。在这里,createApi 希望我们在将缓存缩减器添加到存储时告诉它缓存状态将存在于何处。如果你不提供 reducerPath 选项,则默认为 'api',因此你的所有 RTKQ 缓存数据将存储在 state.api 下。

¥createApi also accepts a reducerPath field, which defines the expected top-level state slice field for the generated reducer. For our other slices like postsSlice, there's no guarantee that it will be used to update state.posts - we could have attached the reducer anywhere in the root state, like someOtherField: postsReducer. Here, createApi expects us to tell it where the cache state will exist when we add the cache reducer to the store. If you don't provide a reducerPath option, it defaults to 'api', so all your RTKQ cache data will be stored under state.api.

如果你忘记将 reducer 添加到存储中,或者将其附加到与 reducerPath 中指定的键不同的键,RTKQ 将记录一条错误,让你知道需要修复此问题。

¥If you forget to add the reducer to the store, or attach it at a different key than what is specified in reducerPath, RTKQ will log an error to let you know this needs to be fixed.

定义端点

¥Defining Endpoints

所有请求的 URL 的第一部分在 fetchBaseQuery 定义中定义为 '/fakeApi'

¥The first part of the URL for all requests is defined as '/fakeApi' in the fetchBaseQuery definition.

第一步,我们想要添加一个端点,该端点将从假 API 服务器返回整个帖子列表。我们将包含一个名为 getPosts 的端点,并使用 builder.query() 将其定义为查询端点。此方法接受许多选项来配置如何发出请求和处理响应。现在,我们需要做的就是通过定义 query 选项来提供 URL 路径的剩余部分,并使用返回 URL 字符串的回调:() => '/posts'

¥For our first step, we want to add an endpoint that will return the entire list of posts from the fake API server. We'll include an endpoint called getPosts, and define it as a query endpoint using builder.query(). This method accepts many options for configuring how to make the request and process the response. For now, all we need to do is supply the remaining piece of the URL path by defining a query option, with a callback that returns the URL string: () => '/posts'.

默认情况下,查询端点将使用 GET HTTP 请求,但你可以通过返回 {url: '/posts', method: 'POST', body: newPost} 之类的对象(而不仅仅是 URL 字符串本身)来覆盖它。你还可以通过这种方式为请求定义其他几个选项,例如设置标头。

¥By default, query endpoints will use a GET HTTP request, but you can override that by returning an object like {url: '/posts', method: 'POST', body: newPost} instead of just the URL string itself. You can also define several other options for the request this way, such as setting headers.

导出 API 切片和钩子

¥Exporting API Slices and Hooks

在我们之前的切片文件中,我们只是导出了动作创建器和切片缩减器,因为这些都是其他文件中所需要的。使用 RTK 查询,我们通常会导出整个 "API 切片" 对象本身,因为它有几个可能有用的字段。

¥In our earlier slice files, we just exported the action creators and the slice reducers, because those are all that's needed in other files. With RTK Query, we typically export the entire "API slice" object itself, because it has several fields that may be useful.

最后,仔细查看该文件的最后一行。这个 useGetPostsQuery 值从哪里来?

¥Finally, look carefully at the last line of this file. Where's this useGetPostsQuery value coming from?

RTK Query 的 React 集成将为我们定义的每个端点自动生成 React hook!这些钩子封装了在组件安装时触发请求的过程,以及在处理请求和数据可用时重新渲染组件的过程。我们可以从这个 API 切片文件中导出这些钩子,以便在我们的 React 组件中使用。

¥RTK Query's React integration will automatically generate React hooks for every endpoint we define! Those hooks encapsulate the process of triggering a request when a component mounts, and re-rendering the component as the request is processed and data is available. We can export those hooks out of this API slice file for use in our React components.

钩子根据标准约定自动命名:

¥The hooks are automatically named based on a standard convention:

  • use,任何 React hook 的正常前缀

    ¥use, the normal prefix for any React hook

  • 端点的名称,大写

    ¥The name of the endpoint, capitalized

  • 端点类型,QueryMutation

    ¥The type of the endpoint, Query or Mutation

在本例中,我们的端点是 getPosts,它是一个查询端点,因此生成的钩子是 useGetPostsQuery

¥In this case, our endpoint is getPosts and it's a query endpoint, so the generated hook is useGetPostsQuery.

配置存储

¥Configuring the Store

我们现在需要将 API 切片连接到 Redux 存储。我们可以修改现有的 store.js 文件,将 API 切片的缓存缩减器添加到状态中。此外,API 切片还会生成需要添加到存储的自定义中间件。这个中间件也必须添加 - 它管理缓存的生命周期和过期时间。

¥We now need to hook up the API slice to our Redux store. We can modify the existing store.js file to add the API slice's cache reducer to the state. Also, the API slice generates a custom middleware that needs to be added to the store. This middleware must be added as well - it manages cache lifetimes and expiration.

app/store.js
import postsReducer from '../features/posts/postsSlice'
import usersReducer from '../features/users/usersSlice'
import notificationsReducer from '../features/notifications/notificationsSlice'
import { apiSlice } from '../features/api/apiSlice'

export default configureStore({
reducer: {
posts: postsReducer,
users: usersReducer,
notifications: notificationsReducer,
[apiSlice.reducerPath]: apiSlice.reducer
},
middleware: getDefaultMiddleware =>
getDefaultMiddleware().concat(apiSlice.middleware)
})

我们可以重用 apiSlice.reducerPath 字段作为 reducer 参数中的计算键,以确保缓存缩减器添加在正确的位置。

¥We can reuse the apiSlice.reducerPath field as a computed key in the reducer parameter, to ensure that the caching reducer is added in the right place.

我们需要在存储设置中保留所有现有的标准中间件(例如 redux-thunk),并且 API 切片的中间件通常会跟踪这些中间件。我们可以通过向 configureStore 提供 middleware 参数,调用提供的 getDefaultMiddleware() 方法,并在返回的中间件数组的末尾添加 apiSlice.middleware 来做到这一点。

¥We need to keep all of the existing standard middleware like redux-thunk in the store setup, and the API slice's middleware typically goes after those. We can do that by supplying the middleware argument to configureStore, calling the provided getDefaultMiddleware() method, and adding apiSlice.middleware at the end of the returned middleware array.

显示带有查询的帖子

¥Displaying Posts with Queries

在组件中使用查询钩子

¥Using Query Hooks in Components

现在我们已经定义了 API 切片并将其添加到存储中,我们可以将生成的 useGetPostsQuery 钩子导入到我们的 <PostsList> 组件中并在那里使用它。

¥Now that we have the API slice defined and added to the store, we can import the generated useGetPostsQuery hook into our <PostsList> component and use it there.

目前,<PostsList> 专门导入 useSelectoruseDispatchuseEffect,从存储中读取帖子数据和加载状态,并在挂载时调度 fetchPosts() thunk 以触发数据获取。useGetPostsQueryHook 取代了这一切!

¥Currently, <PostsList> is specifically importing useSelector, useDispatch, and useEffect, reading posts data and loading state from the store, and dispatching the fetchPosts() thunk on mount to trigger the data fetch. The useGetPostsQueryHook replaces all of that!

让我们看看当我们使用这个钩子时 <PostsList> 是什么样子的:

¥Let's see how <PostsList> looks when we use this hook:

features/posts/PostsList.js
import React from 'react'
import { Link } from 'react-router-dom'

import { Spinner } from '../../components/Spinner'
import { PostAuthor } from './PostAuthor'
import { TimeAgo } from './TimeAgo'
import { ReactionButtons } from './ReactionButtons'

import { useGetPostsQuery } from '../api/apiSlice'

let PostExcerpt = ({ post }) => {
return (
<article className="post-excerpt" key={post.id}>
<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 {
data: posts,
isLoading,
isSuccess,
isError,
error
} = useGetPostsQuery()

let content

if (isLoading) {
content = <Spinner text="Loading..." />
} else if (isSuccess) {
content = posts.map(post => <PostExcerpt key={post.id} post={post} />)
} else if (isError) {
content = <div>{error.toString()}</div>
}

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

从概念上讲,<PostsList> 仍在执行之前的所有相同工作,但我们能够用对 useGetPostsQuery() 的单个调用来替换多个 useSelector 调用和 useEffect 调度。

¥Conceptually, <PostsList> is still doing all the same work it was before, but we were able to replace the multiple useSelector calls and the useEffect dispatch with a single call to useGetPostsQuery().

提示

通常应该使用查询钩子来访问组件中的缓存数据 - 你不应该编写自己的 useSelector 调用来访问获取的数据或编写 useEffect 调用来触发获取!

¥You should normally use the query hooks to access cached data in components - you shouldn't write your own useSelector calls to access fetched data or useEffect calls to trigger fetching!

每个生成的查询钩子都会返回一个包含多个字段的 "result" 对象,包括:

¥Each generated query hook returns a "result" object containing several fields, including:

  • data:来自服务器的实际响应内容。在收到响应之前,该字段将为 undefined

    ¥data: the actual response contents from the server. This field will be undefined until the response is received.

  • isLoading:一个布尔值,指示此钩子当前是否正在向服务器发出第一个请求。(请注意,如果参数更改以请求不同的数据,isLoading 将保持为 false。)

    ¥isLoading: a boolean indicating if this hook is currently making the first request to the server. (Note that if the parameters change to request different data, isLoading will remain false.)

  • isFetching:一个布尔值,指示钩子当前是否正在向服务器发出任何请求

    ¥isFetching: a boolean indicating if the hook is currently making any request to the server

  • isSuccess:一个布尔值,指示钩子是否已成功发出请求并已缓存可用数据(即现在应定义 data

    ¥isSuccess: a boolean indicating if the hook has made a successful request and has cached data available (ie, data should be defined now)

  • isError:一个布尔值,指示最后一个请求是否有错误

    ¥isError: a boolean indicating if the last request had an error

  • error:序列化的错误对象

    ¥error: a serialized error object

通常会解构结果对象中的字段,并可能将 data 重命名为更具体的变量(如 posts)来描述它包含的内容。然后我们可以使用状态布尔值和 data/error 字段来渲染我们想要的 UI。但是,如果你使用 TypeScript,则可能需要按原样保留原始对象,并在条件检查中将标志引用为 result.isSuccess,以便 TS 可以正确推断 data 有效。

¥It's common to destructure fields from the result object, and possibly rename data to a more specific variable like posts to describe what it contains. We can then use the status booleans and the data/error fields to render the UI that we want. However, if you're using TypeScript, you may need to keep the original object as-is and refer to flags as result.isSuccess in your conditional checks, so that TS can correctly infer that data is valid.

之前,我们从存储中选择帖子 ID 列表,将帖子 ID 传递给每个 <PostExcerpt> 组件,然后分别从存储中选择每个单独的 Post 对象。由于 posts 数组已经拥有所有的 post 对象,我们已经切换回将 post 对象本身作为 props 向下传递。

¥Previously, we were selecting a list of post IDs from the store, passing a post ID to each <PostExcerpt> component, and selecting each individual Post object from the store separately. Since the posts array already has all of the post objects, we've switched back to passing the post objects themselves down as props.

对帖子进行排序

¥Sorting Posts

不幸的是,这些帖子现在显示的顺序不正确。以前,我们使用 createEntityAdapter 的排序选项在 reducer 级别按日期对它们进行排序。由于 API 切片只是缓存从服务器返回的确切数组,因此不会发生特定的排序 - 服务器发回的任何订单都是我们所得到的。

¥Unfortunately, the posts are now being displayed out of order. Previously, we were sorting them by date at the reducer level with createEntityAdapter's sorting option. Since the API slice is just caching the exact array returned from the server, there's no specific sorting happening - whatever order the server sent back is what we've got.

对于如何处理这个问题,有几种不同的选择。现在,我们将在 <PostsList> 本身内部进行排序,稍后我们将讨论其他选项及其权衡。

¥There's a few different options for how to handle this. For now, we'll do the sorting inside of <PostsList> itself, and we'll talk about the other options and their tradeoffs later.

我们不能直接调用 posts.sort(),因为 Array.sort() 会改变现有数组,所以我们需要先复制它。为了避免每次重新渲染时都重新排序,我们可以在 useMemo() 钩子中进行排序。我们还想给 posts 一个默认的空数组,以防它是 undefined,这样我们总是有一个数组可以排序。

¥We can't just call posts.sort() directly, because Array.sort() mutates the existing array, so we'll need to make a copy of it first. To avoid re-sorting on every rerender, we can do the sorting in a useMemo() hook. We'll also want to give posts a default empty array in case it's undefined, so that we always have an array to sort on.

features/posts/PostsList.js
// omit setup

export const PostsList = () => {
const {
data: posts = [],
isLoading,
isSuccess,
isError,
error
} = useGetPostsQuery()

const sortedPosts = useMemo(() => {
const sortedPosts = posts.slice()
// Sort posts in descending chronological order
sortedPosts.sort((a, b) => b.date.localeCompare(a.date))
return sortedPosts
}, [posts])

let content

if (isLoading) {
content = <Spinner text="Loading..." />
} else if (isSuccess) {
content = sortedPosts.map(post => <PostExcerpt key={post.id} post={post} />)
} else if (isError) {
content = <div>{error.toString()}</div>
}

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

显示个人帖子

¥Displaying Individual Posts

我们更新了 <PostsList> 以获取所有帖子的列表,并且我们在列表中显示每个 Post 的片段。但是,如果我们单击其中任何一个的 "查看帖子",我们的 <SinglePostPage> 组件将无法在旧的 state.posts 切片中找到帖子,并向我们显示 "帖子未找到!" 错误。我们还需要更新 <SinglePostPage> 以使用 RTK 查询。

¥We've updated <PostsList> to fetch a list of all posts, and we're showing pieces of each Post inside the list. But, if we click on "View Post" for any of them, our <SinglePostPage> component will fail to find a post in the old state.posts slice and show us a "Post not found!" error. We need to update <SinglePostPage> to use RTK Query as well.

我们有几种方法可以做到这一点。一种方法是让 <SinglePostPage> 调用相同的 useGetPostsQuery() 钩子,获取整个帖子数组,然后找到它需要显示的一个 Post 对象。查询钩子还有一个 selectFromResult 选项,它允许我们在钩子本身内部更早地进行相同的查找 - 我们稍后会看到它的实际效果。

¥There's a couple ways we could do this. One would be to have <SinglePostPage> call the same useGetPostsQuery() hook, get the entire array of posts, and find just the one Post object it needs to display. Query hooks also have a selectFromResult option that would allow us to do that same lookup earlier, inside the hook itself - we'll see this in action later.

相反,我们将尝试添加另一个端点定义,该定义将允许我们根据服务器的 ID 请求单个帖子。这有点多余,但它可以让我们看到如何使用 RTK Query 根据参数自定义查询请求。

¥Instead, we're going to try adding another endpoint definition that will let us request a single post from the server based on its ID. This is somewhat redundant, but it will allow us to see how RTK Query can be used to customize query requests based on arguments.

添加单个 Post 查询端点

¥Adding the Single Post Query Endpoint

apiSlice.js 中,我们将添加另一个查询端点定义,称为 getPost(这次没有“s”):

¥In apiSlice.js, we're going to add another query endpoint definition, called getPost (no 's' this time):

features/api/apiSlice.js
export const apiSlice = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/fakeApi' }),
endpoints: builder => ({
getPosts: builder.query({
query: () => '/posts'
}),
getPost: builder.query({
query: postId => `/posts/${postId}`
})
})
})

export const { useGetPostsQuery, useGetPostQuery } = apiSlice

getPost 端点看起来很像现有的 getPosts 端点,但 query 参数不同。这里,query 采用一个名为 postId 的参数,我们使用 postId 来构造服务器 URL。这样我们就可以向服务器发出仅针对一个特定 Post 对象的请求。

¥The getPost endpoint looks much like the existing getPosts endpoint, but the query parameter is different. Here, query takes an argument called postId, and we're using that postId to construct the server URL. That way we can make a server request for just one specific Post object.

这还会生成一个新的 useGetPostQuery 钩子,因此我们也将其导出。

¥This also generates a new useGetPostQuery hook, so we export that as well.

查询参数和缓存键

¥Query Arguments and Cache Keys

我们的 <SinglePostPage> 当前正在根据 ID 从 state.posts 读取一个 Post 条目。我们需要更新它以调用新的 useGetPostQuery 钩子,并使用与主列表类似的加载状态。

¥Our <SinglePostPage> is currently reading one Post entry from state.posts based on ID. We need to update it to call the new useGetPostQuery hook, and use similar loading state as the main list.

features/posts/SinglePostPage.js
import React from 'react'
import { Link } from 'react-router-dom'

import { Spinner } from '../../components/Spinner'
import { useGetPostQuery } from '../api/apiSlice'

import { PostAuthor } from './PostAuthor'
import { TimeAgo } from './TimeAgo'
import { ReactionButtons } from './ReactionButtons'

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

const { data: post, isFetching, isSuccess } = useGetPostQuery(postId)

let content
if (isFetching) {
content = <Spinner text="Loading..." />
} else if (isSuccess) {
content = (
<article className="post">
<h2>{post.title}</h2>
<div>
<PostAuthor userId={post.user} />
<TimeAgo timestamp={post.date} />
</div>
<p className="post-content">{post.content}</p>
<ReactionButtons post={post} />
<Link to={`/editPost/${post.id}`} className="button">
Edit Post
</Link>
</article>
)
}

return <section>{content}</section>
}

请注意,我们正在获取从路由匹配中读取的 postId,并将其作为参数传递给 useGetPostQuery。然后,查询钩子将使用它来构造请求 URL,并获取这个特定的 Post 对象。

¥Notice that we're taking the postId we've read from the router match, and passing it as an argument to useGetPostQuery. The query hook will then use that to construct the request URL, and fetch this specific Post object.

那么,所有这些数据是如何缓存的呢?让我们点击 "查看帖子" 查看我们的帖子条目之一,然后看看此时 Redux 存储中的内容。

¥So how is all this data being cached, anyway? Let's click "View Post" for one of our post entries, then take a look at what's inside the Redux store at this point.

RTK Query data cached in the store state

我们可以看到我们有一个顶层 state.api 切片,正如存储设置所预期的那样。里面有一个名为 queries 的部分,目前有两个项目。密钥 getPosts(undefined) 代表我们通过 getPosts 端点发出的请求的元数据和响应内容。同样,密钥 getPost('abcd1234') 是针对我们刚刚为这篇文章提出的特定请求。

¥We can see that we have a top-level state.api slice, as expected from the store setup. Inside of there is a section called queries, and it currently has two items. The key getPosts(undefined) represents the metadata and response contents for the request we made with the getPosts endpoint. Similarly, the key getPost('abcd1234') is for the specific request we just made for this one post.

RTK 查询为每个唯一端点+参数组合创建一个 "缓存键",并单独存储每个缓存键的结果。这意味着你可以多次使用同一个查询钩子,向其传递不同的查询参数,并且每个结果将单独缓存在 Redux 存储中。

¥RTK Query creates a "cache key" for each unique endpoint + argument combination, and stores the results for each cache key separately. That means that you can use the same query hook multiple times, pass it different query parameters, and each result will be cached separately in the Redux store.

提示

如果你需要在多个组件中使用相同的数据,只需在每个组件中使用相同的参数调用相同的查询钩子即可!例如,你可以在三个不同的组件中调用 useGetPostQuery('123'),RTK Query 将确保数据仅获取一次,并且每个组件将根据需要重新渲染。

¥If you need the same data in multiple components, just call the same query hook with the same arguments in each component! For example, you can call useGetPostQuery('123') in three different components, and RTK Query will make sure the data is only fetched once, and each component will re-render as needed.

还需要注意的是,查询参数必须是单个值!如果需要传递多个参数,则必须传递包含多个字段的对象(与 createAsyncThunk 完全相同)。RTK 查询将对字段进行 "浅稳定" 比较,如果其中任何一个字段发生更改,则重新获取数据。

¥It's also important to note that the query parameter must be a single value! If you need to pass through multiple parameters, you must pass an object containing multiple fields (exactly the same as with createAsyncThunk). RTK Query will do a "shallow stable" comparison of the fields, and re-fetch the data if any of them have changed.

请注意,左侧列表中的操作名称更加通用且描述性较差:api/executeQuery/fulfilled,而不是 posts/fetchPosts/fulfilled。这是使用额外抽象层的权衡。各个操作确实包含 action.meta.arg.endpointName 下的特定端点名称,但在操作历史记录列表中不那么容易查看。

¥Notice that the names of the actions in the left-hand list are much more generic and less descriptive: api/executeQuery/fulfilled, instead of posts/fetchPosts/fulfilled. This is a tradeoff of using an additional abstraction layer. The individual actions do contain the specific endpoint name under action.meta.arg.endpointName, but it's not as easily viewable in the action history list.

提示

Redux DevTools 有一个 "RTK 查询" 选项卡,专门以更可用的格式显示 RTK 查询数据。这包括每个端点和缓存结果的信息、查询时间的统计信息等等:

¥The Redux DevTools have an "RTK Query" tab that specifically shows RTK Query data in a more usable format. This includes info on each endpoint and cache result, stats on query timing, and much more:

创建带有突变的帖子

¥Creating Posts with Mutations

我们已经了解了如何通过定义 "query" 端点从服务器获取数据,但是如何向服务器发送更新呢?

¥We've seen how we can fetch data from the server by defining "query" endpoints, but what about sending updates to the server?

RTK 查询让我们定义更新服务器上数据的突变端点。让我们添加一个突变,让我们添加一个新帖子。

¥RTK Query lets us define mutation endpoints that update data on the server. Let's add a mutation that will let us add a new post.

添加新的突变后端点

¥Adding the New Post Mutation Endpoint

添加突变端点与添加查询端点非常相似。最大的区别是我们使用 builder.mutation() 而不是 builder.query() 来定义端点。另外,我们现在需要将 HTTP 方法更改为 'POST' 请求,并且还必须提供请求正文。

¥Adding a mutation endpoint is very similar to adding a query endpoint. The biggest difference is that we define the endpoint using builder.mutation() instead of builder.query(). Also, we now need to change the HTTP method to be a 'POST' request, and we have to provide the body of the request as well.

features/api/apiSlice.js
export const apiSlice = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/fakeApi' }),
endpoints: builder => ({
getPosts: builder.query({
query: () => '/posts'
}),
getPost: builder.query({
query: postId => `/posts/${postId}`
}),
addNewPost: builder.mutation({
query: initialPost => ({
url: '/posts',
method: 'POST',
// Include the entire post object as the body of the request
body: initialPost
})
})
})
})

export const {
useGetPostsQuery,
useGetPostQuery,
useAddNewPostMutation
} = apiSlice

这里我们的 query 选项返回一个包含 {url, method, body} 的对象。由于我们使用 fetchBaseQuery 发出请求,因此 body 字段将自动为我们进行 JSON 序列化。

¥Here our query option returns an object containing {url, method, body}. Since we're using fetchBaseQuery to make the requests, the body field will automatically be JSON-serialized for us.

与查询端点一样,API 切片会自动为突变端点生成一个 React hook - 在这种情况下,useAddNewPostMutation

¥Like with query endpoints, the API slice automatically generates a React hook for the mutation endpoint - in this case, useAddNewPostMutation.

在组件中使用 Mutation Hook

¥Using Mutation Hooks in Components

每当我们单击 "保存帖子" 按钮时,我们的 <AddPostForm> 就已经发送了一个异步 thunk 来添加帖子。为此,它必须导入 useDispatchaddNewPost thunk。突变钩子取代了这两者,并且使用模式非常相似。

¥Our <AddPostForm> is already dispatching an async thunk to add a post whenever we click the "Save Post" button. To do that, it has to import useDispatch and the addNewPost thunk. The mutation hooks replace both of those, and the usage pattern is very similar.

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

import { Spinner } from '../../components/Spinner'
import { useAddNewPostMutation } from '../api/apiSlice'
import { selectAllUsers } from '../users/usersSlice'

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

const [addNewPost, { isLoading }] = useAddNewPostMutation()
const users = useSelector(selectAllUsers)

const onTitleChanged = e => setTitle(e.target.value)
const onContentChanged = e => setContent(e.target.value)
const onAuthorChanged = e => setUserId(e.target.value)

const canSave = [title, content, userId].every(Boolean) && !isLoading

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

// omit rendering logic
}

突变钩子返回一个包含两个值的数组:

¥Mutation hooks return an array with two values:

  • 第一个值是 "触发功能"。调用时,它会使用你提供的任何参数向服务器发出请求。这实际上就像一个已经被封装以立即调度自身的 thunk。

    ¥The first value is a "trigger function". When called, it makes the request to the server, with whatever argument you provide. This is effectively like a thunk that has already been wrapped to immediately dispatch itself.

  • 第二个值是一个对象,其中包含有关当前正在进行的请求的元数据(如果有)。这包括一个 isLoading 标志,用于指示请求是否正在进行中。

    ¥The second value is an object with metadata about the current in-progress request, if any. This includes an isLoading flag to indicate if a request is in-progress.

我们可以用 useAddNewPostMutation 钩子中的触发函数和 isLoading 标志替换现有的 thunk 调度和组件加载状态,组件的其余部分保持不变。

¥We can replace the existing thunk dispatch and component loading state with the trigger function and isLoading flag from the useAddNewPostMutation hook, and the rest of the component stays the same.

与 thunk 调度一样,我们使用初始 post 对象调用 addNewPost。这会返回一个带有 .unwrap() 方法的特殊 Promise,我们可以 await addNewPost().unwrap() 使用标准 try/catch 块来处理任何潜在的错误。

¥As with the thunk dispatch, we call addNewPost with the initial post object. This returns a special Promise with a .unwrap() method, and we can await addNewPost().unwrap() to handle any potential errors with a standard try/catch block.

刷新缓存数据

¥Refreshing Cached Data

当我们点击 "保存帖子" 时,我们可以在浏览器 DevTools 中查看 Network 选项卡,确认 HTTP POST 请求成功。但是,如果我们回到那里,新帖子不会出现在我们的 <PostsList> 中。我们在内存中仍然有相同的缓存数据。

¥When we click "Save Post", we can view the Network tab in the browser DevTools and confirm that the HTTP POST request succeeded. But, the new post isn't showing up in our <PostsList> if we go back there. We still have the same cached data in memory.

我们需要告诉 RTK Query 刷新其缓存的帖子列表,以便我们可以看到刚刚添加的新帖子。

¥We need to tell RTK Query to refresh its cached list of posts so that we can see the new post we just added.

手动重新获取帖子

¥Refetching Posts Manually

第一个选项是手动强制 RTK 查询重新获取给定端点的数据。查询钩子结果对象包含一个 refetch 函数,我们可以调用该函数来强制重新获取。我们可以暂时在 <PostsList> 上添加一个 "重新获取帖子" 按钮,然后在添加新帖子后点击该按钮。

¥The first option is to manually force RTK Query to refetch data for a given endpoint. Query hook result objects include a refetch function that we can call to force a refetch. We can temporarily add a "Refetch Posts" button to <PostsList> and click that after adding a new post.

另外,之前我们看到查询钩子既有 isLoading 标志(如果这是第一次数据请求,则为 true),也有 isFetching 标志(如果正在进行任何数据请求,则为 true)。我们可以查看 isFetching 标志,并在重新获取过程中再次用加载加载控件替换整个帖子列表。但是,这可能有点烦人,而且除此之外 - 我们已经有了所有这些帖子,为什么要完全隐藏它们呢?

¥Also, earlier we saw that query hooks have both an isLoading flag, which is true if this is the first request for data, and an isFetching flag, which is true while any request for data is in progress. We could look at the isFetching flag, and replace the entire list of posts with a loading spinner again while the refetch is in progress. But, that could be a bit annoying, and besides - we already have all these posts, why should we completely hide them?

相反,我们可以使现有的帖子列表部分透明以指示数据已过时,但在重新获取时保持它们可见。请求完成后,我们就可以恢复正常显示帖子列表。

¥Instead, we could make the existing list of posts partially transparent to indicate the data is stale, but keep them visible while the refetch is happening. As soon as the request completes, we can return to showing the posts list as normal.

features/posts/PostsList.js
import React, { useMemo } from 'react'
import { Link } from 'react-router-dom'
import classnames from 'classnames'

// omit other imports and PostExcerpt

export const PostsList = () => {
const {
data: posts = [],
isLoading,
isFetching,
isSuccess,
isError,
error,
refetch
} = useGetPostsQuery()

const sortedPosts = useMemo(() => {
const sortedPosts = posts.slice()
sortedPosts.sort((a, b) => b.date.localeCompare(a.date))
return sortedPosts
}, [posts])

let content

if (isLoading) {
content = <Spinner text="Loading..." />
} else if (isSuccess) {
const renderedPosts = sortedPosts.map(post => (
<PostExcerpt key={post.id} post={post} />
))

const containerClassname = classnames('posts-container', {
disabled: isFetching
})

content = <div className={containerClassname}>{renderedPosts}</div>
} else if (isError) {
content = <div>{error.toString()}</div>
}

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

如果我们添加一个新帖子,然后单击 "重新获取帖子",我们现在应该看到帖子列表变为半透明几秒钟,然后重新渲染,并在顶部添加新帖子。

¥If we add a new post and then click "Refetch Posts", we should now see the posts list go semi-transparent for a couple seconds, then re-render with the new post added at the top.

缓存失效自动刷新

¥Automatic Refreshing with Cache Invalidation

让用户手动点击重新获取数据有时是必要的,但对于正常使用来说绝对不是一个好的解决方案。

¥Having users manually click to refetch data is occasionally necessary, but definitely not a good solution for normal usage.

我们知道我们的 "server" 拥有所有帖子的完整列表,包括我们刚刚添加的帖子。理想情况下,我们希望我们的应用在突变请求完成后立即自动重新获取更新的帖子列表。这样我们就知道客户端缓存的数据与服务器的数据同步。

¥We know that our "server" has a complete list of all posts, including the one we just added. Ideally, we want to have our app automatically refetch the updated list of posts as soon as the mutation request has completed. That way we know our client-side cached data is in sync with what the server has.

RTK 查询让我们可以使用 "tags" 定义查询和突变之间的关系,以实现自动数据重新获取。"tag" 是一个字符串或小对象,可让你命名某些类型的数据并使部分缓存无效。当缓存标签失效时,RTK 查询将自动重新获取标有该标签的终端。

¥RTK Query lets us define relationships between queries and mutations to enable automatic data refetching, using "tags". A "tag" is a string or small object that lets you name certain types of data, and invalidate portions of the cache. When a cache tag is invalidated, RTK Query will automatically refetch the endpoints that were marked with that tag.

基本标签的使用需要向我们的 API 切片添加三条信息:

¥Basic tag usage requires adding three pieces of information to our API slice:

  • API 切片对象中的根 tagTypes 字段,声明 'Post' 等数据类型的字符串标记名称数组

    ¥A root tagTypes field in the API slice object, declaring an array of string tag names for data types such as 'Post'

  • 查询端点中的 providesTags 数组,列出了描述该查询中的数据的一组标签

    ¥A providesTags array in query endpoints, listing a set of tags describing the data in that query

  • 突变端点中的 invalidatesTags 数组,列出每次突变运行时都会失效的一组标签

    ¥An invalidatesTags array in mutation endpoints, listing a set of tags that are invalidated every time that mutation runs

我们可以将一个名为 'Post' 的标签添加到我们的 API 切片中,这样我们就可以在添加新帖子时自动重新获取 getPosts 端点:

¥We can add a single tag called 'Post' to our API slice that will let us automatically refetch our getPosts endpoint any time we add a new post:

features/api/apiSlice.js
export const apiSlice = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/fakeApi' }),
tagTypes: ['Post'],
endpoints: builder => ({
getPosts: builder.query({
query: () => '/posts',
providesTags: ['Post']
}),
getPost: builder.query({
query: postId => `/posts/${postId}`
}),
addNewPost: builder.mutation({
query: initialPost => ({
url: '/posts',
method: 'POST',
body: initialPost
}),
invalidatesTags: ['Post']
})
})
})

这就是我们所需要的!现在,如果我们单击 "保存帖子",你应该会看到 <PostsList> 组件在几秒钟后自动变灰,然后使用顶部新添加的帖子重新渲染。

¥That's all we need! Now, if we click "Save Post", you should see the <PostsList> component automatically gray out after a couple seconds, and then rerender with the newly added post at the top.

请注意,这里的字面量字符串 'Post' 没有什么特别的。我们可以将其称为 'Fred''qwerty' 或其他任何名称。只需要每个字段中的字符串相同,以便 RTK 查询知道 "当发生此突变时,使列出的具有相同标记字符串的所有端点无效"。

¥Note that there's nothing special about the literal string 'Post' here. We could have called it 'Fred', 'qwerty', or anything else. It just needs to be the same string in each field, so that RTK Query knows "when this mutation happens, invalidate all endpoints that have that same tag string listed".

你学到了什么

¥What You've Learned

通过 RTK 查询,如何管理数据获取、缓存和加载状态的实际细节被抽象出来。这大大简化了应用代码,并使我们能够专注于有关预期应用行为的更高级别的关注点。由于 RTK 查询是使用我们已经见过的相同 Redux Toolkit API 实现的,因此我们仍然可以使用 Redux DevTools 来查看状态随时间的变化。

¥With RTK Query, the actual details of how to manage data fetching, caching, and loading state are abstracted away. This simplifies application code considerably, and lets us focus on higher-level concerns about intended app behavior instead. Since RTK Query is implemented using the same Redux Toolkit APIs we've already seen, we can still use the Redux DevTools to view the changes in our state over time.

概括
  • RTK Query 是 Redux Toolkit 中包含的数据获取和缓存解决方案

    ¥RTK Query is a data fetching and caching solution included in Redux Toolkit

    • RTK Query 为你抽象了管理缓存服务器数据的过程,无需编写加载状态、存储结果和发出请求的逻辑

      ¥RTK Query abstracts the process of managing cached server data for you, and eliminates the need to write logic for loading state, storing results, and making requests

    • RTK 查询构建在 Redux 中使用的相同模式之上,例如异步 thunk

      ¥RTK Query builds on top of the same patterns used in Redux, like async thunks

  • RTK 查询每个应用使用一个 "API 切片",使用 createApi 定义

    ¥RTK Query uses a single "API slice" per application, defined using createApi

    • RTK Query 提供与 UI 无关且特定于 React 的 createApi 版本

      ¥RTK Query provides UI-agnostic and React-specific versions of createApi

    • API 切片定义了多个 "endpoints" 用于不同的服务器操作

      ¥API slices define multiple "endpoints" for different server operations

    • 如果使用 React 集成,API 切片包含自动生成的 React 钩子

      ¥The API slice includes auto-generated React hooks if using the React integration

  • 查询端点允许从服务器获取和缓存数据

    ¥Query endpoints allow fetching and caching data from the server

    • 查询钩子返回 data 值以及加载状态标志

      ¥Query hooks return a data value, plus loading status flags

    • 查询可以手动重新获取,也可以使用 "tags" 自动进行缓存失效

      ¥The query can be re-fetched manually, or automatically using "tags" for cache invalidation

  • 突变端点允许更新服务器上的数据

    ¥Mutation endpoints allow updating data on the server

    • 突变钩子返回发送更新请求的 "trigger" 函数以及加载状态

      ¥Mutation hooks return a "trigger" function that sends an update request, plus loading status

    • 触发函数返回一个 Promise,可以是 "unwrapped" 并等待

      ¥The trigger function returns a Promise that can be "unwrapped" and awaited

下一步是什么?

¥What's Next?

RTK 查询提供可靠的默认行为,但还包括许多用于自定义请求管理方式和使用缓存数据的选项。在 第 8 部分:RTK 查询高级模式 中,我们将了解如何使用这些选项来实现有用的功能,例如乐观更新。

¥RTK Query provides solid default behavior, but also includes many options for customizing how requests are managed and working with cached data. In Part 8: RTK Query Advanced Patterns, we'll see how to use these options to implement useful features like optimistic updates.