Usage > Queries: fetching data from a server">Usage > Queries: fetching data from a server">
跳至主要内容

查询

概述

这是 RTK Query 最常见的用例。查询操作可以使用您选择的任何数据获取库执行,但一般建议您只将查询用于检索数据的请求。对于任何更改服务器上的数据或可能使缓存失效的操作,您应该使用 变异

默认情况下,RTK Query 附带 fetchBaseQuery,它是一个轻量级的 fetch 包装器,它会自动处理请求头和响应解析,类似于 axios 等常用库。如果 fetchBaseQuery 不能满足您的需求,请参阅 自定义查询

信息

根据您的环境,如果您选择使用 fetchBaseQueryfetch 本身,您可能需要使用 node-fetchcross-fetchfetch 进行 polyfill。

有关钩子签名和更多详细信息,请参阅 useQuery

定义查询端点

查询端点通过在 createApiendpoints 部分返回一个对象并使用 builder.query() 方法定义字段来定义。

查询端点应定义一个 query 回调函数,该函数构造 URL(包括任何 URL 查询参数),或 一个 queryFn 回调函数,该函数可以执行任意异步逻辑并返回结果。

如果 query 回调函数需要其他数据来生成 URL,则应将其编写为接受单个参数。如果您需要传入多个参数,请将它们格式化为单个“选项对象”传入。

查询端点还可以修改响应内容,然后再将结果缓存,定义“标签”以识别缓存失效,并提供缓存条目生命周期回调函数,以在添加和删除缓存条目时运行其他逻辑。

在与 TypeScript 一起使用时,您应该为返回类型和预期的查询参数提供泛型:build.query<ReturnType, ArgType>。如果没有参数,请使用 void 作为参数类型。

所有查询端点选项的示例
// Or from '@reduxjs/toolkit/query/react'
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
import type { Post } from './types'

const api = createApi({
baseQuery: fetchBaseQuery({
baseUrl: '/',
}),
tagTypes: ['Post'],
endpoints: (build) => ({
// The query accepts a number and returns a Post
getPost: build.query<Post, number>({
// note: an optional `queryFn` may be used in place of `query`
query: (id) => ({ url: `post/${id}` }),
// Pick out data and prevent nested properties in a hook or selector
transformResponse: (response: { data: Post }, meta, arg) => response.data,
// Pick out errors and prevent nested properties in a hook or selector
transformErrorResponse: (
response: { status: string | number },
meta,
arg
) => response.status,
providesTags: (result, error, id) => [{ type: 'Post', id }],
// The 2nd parameter is the destructured `QueryLifecycleApi`
async onQueryStarted(
arg,
{
dispatch,
getState,
extra,
requestId,
queryFulfilled,
getCacheEntry,
updateCachedData,
}
) {},
// The 2nd parameter is the destructured `QueryCacheLifecycleApi`
async onCacheEntryAdded(
arg,
{
dispatch,
getState,
extra,
requestId,
cacheEntryRemoved,
cacheDataLoaded,
getCacheEntry,
updateCachedData,
}
) {},
}),
}),
})

使用 React 钩子执行查询

如果您使用的是 React Hooks,RTK Query 会为您做一些额外的事情。主要好处是您将获得一个渲染优化的钩子,它允许您进行“后台获取”,以及为了方便起见提供派生的布尔值

钩子是根据服务定义中endpoint的名称自动生成的。带有getPost: builder.query()的端点字段将生成一个名为useGetPostQuery的钩子。

钩子类型

有 5 个与查询相关的钩子

  1. useQuery
    • 组合了useQuerySubscriptionuseQueryState,是主要的钩子。自动触发从端点获取数据,将组件“订阅”到缓存的数据,并从 Redux 存储中读取请求状态和缓存的数据。
  2. useQuerySubscription
    • 返回一个refetch函数,并接受所有钩子选项。自动触发从端点获取数据,并将组件“订阅”到缓存的数据。
  3. useQueryState
    • 返回查询状态,并接受skipselectFromResult。从 Redux 存储中读取请求状态和缓存的数据。
  4. useLazyQuery
    • 返回一个包含trigger函数、查询结果和最后承诺信息的元组。类似于useQuery,但可以手动控制数据获取的时间。注意:trigger函数在您希望在已存在缓存数据的情况下跳过发出请求时,会接受第二个参数preferCacheValue?: boolean
  5. useLazyQuerySubscription
    • 返回一个包含trigger函数和最后承诺信息的元组。类似于useQuerySubscription,但可以手动控制数据获取的时间。注意:trigger函数在您希望在已存在缓存数据的情况下跳过发出请求时,会接受第二个参数preferCacheValue?: boolean

在实践中,标准的基于useQuery的钩子,例如useGetPostQuery,将是您应用程序中使用的主要钩子,但其他钩子可用于特定用例。

查询钩子选项

查询钩子期望两个参数:(queryArg?, queryOptions?)

queryArg参数将传递给底层的query回调以生成 URL。

queryOptions对象接受几个额外的参数,可用于控制数据获取的行为

  • skip - 允许查询“跳过”对该渲染的运行。默认为false
  • pollingInterval - 允许查询以提供的间隔自动重新获取,以毫秒为单位指定。默认为0 (关闭)
  • selectFromResult - 允许更改钩子的返回值以获取结果的子集,针对返回的子集进行渲染优化。
  • refetchOnMountOrArgChange - 允许在挂载时强制查询始终重新获取(当提供 true 时)。允许在上次针对相同缓存的查询后经过足够时间(以秒为单位)时强制查询重新获取(当提供 number 时)。默认值为 false
  • refetchOnFocus - 允许在浏览器窗口重新获得焦点时强制查询重新获取。默认值为 false
  • refetchOnReconnect - 允许在重新获得网络连接时强制查询重新获取。默认值为 false
信息

所有与 refetch 相关的选项将覆盖您可能在 createApi 中设置的默认值

常用查询钩子返回值

查询钩子返回一个对象,其中包含诸如查询请求的最新 data 以及当前请求生命周期状态的状态布尔值之类的属性。以下是其中一些最常用的属性。请参考 useQuery 以获取所有返回属性的完整列表。

  • data - 无论钩子参数如何,最新的返回结果,如果存在。
  • currentData - 当前钩子参数的最新返回结果,如果存在。
  • error - 如果存在,则为错误结果。
  • isUninitialized - 当为 true 时,表示查询尚未开始。
  • isLoading - 当为 true 时,表示查询当前正在首次加载,并且还没有数据。这将对第一次发出的请求为 true,但不会对后续请求为 true
  • isFetching - 当为 true 时,表示查询当前正在获取,但可能具有来自早期请求的数据。这将对第一次发出的请求以及后续请求为 true
  • isSuccess - 当为 true 时,表示查询具有来自成功请求的数据。
  • isError - 当为 true 时,表示查询处于 error 状态。
  • refetch - 用于强制重新获取查询的函数

在大多数情况下,您可能需要读取data以及isLoadingisFetching来渲染您的UI。

查询钩子使用示例

这是一个PostDetail组件的示例

示例
export const PostDetail = ({ id }: { id: string }) => {
const {
data: post,
isFetching,
isLoading,
} = useGetPostQuery(id, {
pollingInterval: 3000,
refetchOnMountOrArgChange: true,
skip: false,
})

if (isLoading) return <div>Loading...</div>
if (!post) return <div>Missing post!</div>

return (
<div>
{post.name} {isFetching ? '...refetching' : ''}
</div>
)
}

此组件的设置方式具有一些不错的特性

  1. 它只在首次加载时显示“加载中...”
    • 首次加载定义为一个处于挂起状态且缓存中没有数据的查询
  2. 当请求被轮询间隔重新触发时,它将在帖子名称中添加“...正在重新获取”
  3. 如果用户关闭了此PostDetail,但在允许的时间内重新打开它,他们将立即获得缓存的结果,轮询将恢复之前的行为。

查询加载状态

由特定于 React 的createApi版本创建的自动生成的 React 钩子提供了派生布尔值,这些布尔值反映了给定查询的当前状态。派生布尔值优先于生成的 React 钩子,而不是status标志,因为派生布尔值能够提供更多细节,而这些细节是单个status标志无法提供的,因为多个状态可能在给定时间为真(例如isFetchingisSuccess)。

对于查询端点,RTK Query 在isLoadingisFetching之间保持语义上的区别,以便为提供的派生信息提供更大的灵活性。

  • isLoading指的是查询对于给定钩子首次处于飞行状态。此时将没有数据可用。
  • isFetching指的是查询对于给定端点 + 查询参数组合处于飞行状态,但不一定是首次。数据可能来自此钩子之前完成的早期请求,可能是使用之前的查询参数。

这种区别允许在处理 UI 行为时获得更大的控制。例如,isLoading可用于在首次加载时显示骨架,而isFetching可用于在从第 1 页更改到第 2 页或数据失效并重新获取时将旧数据变灰。

使用查询加载状态管理 UI 行为
import { Skeleton } from './Skeleton'
import { useGetPostsQuery } from './api'

function App() {
const { data = [], isLoading, isFetching, isError } = useGetPostsQuery()

if (isError) return <div>An error has occurred!</div>

if (isLoading) return <Skeleton />

return (
<div className={isFetching ? 'posts--disabled' : ''}>
{data.map((post) => (
<Post
key={post.id}
id={post.id}
name={post.name}
disabled={isFetching}
/>
))}
</div>
)
}

虽然在大多数情况下预期使用data,但currentData也提供,它允许更细粒度的控制。例如,如果您想在 UI 中以半透明的方式显示数据以表示重新获取状态,您可以将dataisFetching结合使用来实现。但是,如果您还希望显示与当前参数相对应的数据,则可以使用currentData来实现。

在下面的示例中,如果第一次获取帖子,将显示加载骨架。如果当前用户的帖子之前已经获取过,并且正在重新获取(例如,由于突变),UI 将显示之前的数据,但会将数据灰显。如果用户更改,它将改为再次显示骨架,而不是将之前用户的​​数据灰显。

使用 currentData 管理 UI 行为
import { Skeleton } from './Skeleton'
import { useGetPostsByUserQuery } from './api'

function PostsList({ userName }: { userName: string }) {
const { currentData, isFetching, isError } = useGetPostsByUserQuery(userName)

if (isError) return <div>An error has occurred!</div>

if (isFetching && !currentData) return <Skeleton />

return (
<div className={isFetching ? 'posts--disabled' : ''}>
{currentData
? currentData.map((post) => (
<Post
key={post.id}
id={post.id}
name={post.name}
disabled={isFetching}
/>
))
: 'No data available'}
</div>
)
}

查询缓存键

当您执行查询时,RTK Query 会自动序列化请求参数并为请求创建一个内部queryCacheKey。任何产生相同queryCacheKey的未来请求都将与原始请求去重,并且如果从任何订阅组件触发查询的refetch,将共享更新。

从查询结果中选择数据

有时您可能有一个父组件订阅了查询,然后在子组件中您想从该查询中选择一个项目。在大多数情况下,您不希望在您知道已经拥有结果的情况下执行额外的getItemById类型查询。

selectFromResult允许您以高性能的方式从查询结果中获取特定部分。使用此功能时,除非所选项目的底层数据发生变化,否则组件不会重新渲染。如果所选项目是较大集合中的一个元素,它将忽略对同一集合中元素的更改。

使用 selectFromResult 提取单个结果
function PostsList() {
const { data: posts } = api.useGetPostsQuery()

return (
<ul>
{posts?.data?.map((post) => <PostById key={post.id} id={post.id} />)}
</ul>
)
}

function PostById({ id }: { id: number }) {
// Will select the post with the given id, and will only rerender if the given post's data changes
const { post } = api.useGetPostsQuery(undefined, {
selectFromResult: ({ data }) => ({
post: data?.find((post) => post.id === id),
}),
})

return <li>{post?.name}</li>
}

请注意,对selectFromResult的整体返回值执行浅层相等性检查,以确定是否强制重新渲染。也就是说,如果返回对象的任何值更改了引用,它将触发重新渲染。如果在回调中创建并使用了一个新的数组/对象作为返回值,由于每次运行回调时都被识别为新项目,因此会阻碍性能优势。当有意提供一个空数组/对象时,为了避免每次运行回调时重新创建它,可以在组件外部声明一个空数组/对象,以保持稳定的引用。

使用带有稳定空数组的 selectFromResult
// An array declared here will maintain a stable reference rather than be re-created again
const emptyArray: Post[] = []

function PostsList() {
// This call will result in an initial render returning an empty array for `posts`,
// and a second render when the data is received.
// It will trigger additional rerenders only if the `posts` data changes
const { posts } = api.useGetPostsQuery(undefined, {
selectFromResult: ({ data }) => ({
posts: data ?? emptyArray,
}),
})

return (
<ul>
{posts.map((post) => (
<PostById key={post.id} id={post.id} />
))}
</ul>
)
}

总结以上行为 - 返回值必须正确记忆。有关更多信息,请参阅 使用选择器派生数据Redux Essentials - RTK Query 高级模式

避免不必要的请求

默认情况下,如果您添加一个与现有组件执行相同查询的组件,将不会执行任何请求。

在某些情况下,您可能希望跳过此行为并强制重新获取 - 在这种情况下,您可以调用挂钩返回的refetch

信息

如果您没有使用 React Hooks,您可以像这样访问refetch

const { status, data, error, refetch } = dispatch(
pokemonApi.endpoints.getPokemon.initiate('bulbasaur'),
)

示例:观察缓存行为

此示例演示了请求去重和缓存行为

  1. 第一个Pokemon组件挂载并立即获取“bulbasaur”
  2. 一秒钟后,另一个Pokemon组件使用“bulbasaur”渲染
    • 请注意,它从未显示“加载中...”并且没有发生新的网络请求?它在这里使用缓存。
  3. 片刻之后,添加了“pikachu”的Pokemon组件,并发生了一个新的请求。
  4. 当您单击特定Pokemon的“重新获取”时,它将使用一个请求更新该Pokemon的所有实例。
试试看

单击“添加 bulbasaur”按钮。您将观察到上面描述的相同行为,直到您单击其中一个组件上的“重新获取”按钮。