查询
概述
这是 RTK Query 最常见的用例。查询操作可以使用您选择的任何数据获取库执行,但一般建议您只将查询用于检索数据的请求。对于任何更改服务器上的数据或可能使缓存失效的操作,您应该使用 变异。
默认情况下,RTK Query 附带 fetchBaseQuery
,它是一个轻量级的 fetch
包装器,它会自动处理请求头和响应解析,类似于 axios
等常用库。如果 fetchBaseQuery
不能满足您的需求,请参阅 自定义查询。
根据您的环境,如果您选择使用 fetchBaseQuery
或 fetch
本身,您可能需要使用 node-fetch
或 cross-fetch
对 fetch
进行 polyfill。
有关钩子签名和更多详细信息,请参阅 useQuery
。
定义查询端点
查询端点通过在 createApi
的 endpoints
部分返回一个对象并使用 builder.query()
方法定义字段来定义。
查询端点应定义一个 query
回调函数,该函数构造 URL(包括任何 URL 查询参数),或 一个 queryFn
回调函数,该函数可以执行任意异步逻辑并返回结果。
如果 query
回调函数需要其他数据来生成 URL,则应将其编写为接受单个参数。如果您需要传入多个参数,请将它们格式化为单个“选项对象”传入。
查询端点还可以修改响应内容,然后再将结果缓存,定义“标签”以识别缓存失效,并提供缓存条目生命周期回调函数,以在添加和删除缓存条目时运行其他逻辑。
在与 TypeScript 一起使用时,您应该为返回类型和预期的查询参数提供泛型:build.query<ReturnType, ArgType>
。如果没有参数,请使用 void
作为参数类型。
- TypeScript
- JavaScript
// 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,
}
) {},
}),
}),
})
// Or from '@reduxjs/toolkit/query/react'
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
const api = createApi({
baseQuery: fetchBaseQuery({
baseUrl: '/',
}),
tagTypes: ['Post'],
endpoints: (build) => ({
// The query accepts a number and returns a Post
getPost: build.query({
// 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, meta, arg) => response.data,
// Pick out errors and prevent nested properties in a hook or selector
transformErrorResponse: (response, 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 个与查询相关的钩子
useQuery
- 组合了
useQuerySubscription
和useQueryState
,是主要的钩子。自动触发从端点获取数据,将组件“订阅”到缓存的数据,并从 Redux 存储中读取请求状态和缓存的数据。
- 组合了
useQuerySubscription
- 返回一个
refetch
函数,并接受所有钩子选项。自动触发从端点获取数据,并将组件“订阅”到缓存的数据。
- 返回一个
useQueryState
- 返回查询状态,并接受
skip
和selectFromResult
。从 Redux 存储中读取请求状态和缓存的数据。
- 返回查询状态,并接受
useLazyQuery
- 返回一个包含
trigger
函数、查询结果和最后承诺信息的元组。类似于useQuery
,但可以手动控制数据获取的时间。注意:trigger
函数在您希望在已存在缓存数据的情况下跳过发出请求时,会接受第二个参数preferCacheValue?: boolean
。
- 返回一个包含
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
以及isLoading
或isFetching
来渲染您的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>
)
}
此组件的设置方式具有一些不错的特性
- 它只在首次加载时显示“加载中...”
- 首次加载定义为一个处于挂起状态且缓存中没有数据的查询
- 当请求被轮询间隔重新触发时,它将在帖子名称中添加“...正在重新获取”
- 如果用户关闭了此
PostDetail
,但在允许的时间内重新打开它,他们将立即获得缓存的结果,轮询将恢复之前的行为。
查询加载状态
由特定于 React 的createApi
版本创建的自动生成的 React 钩子提供了派生布尔值,这些布尔值反映了给定查询的当前状态。派生布尔值优先于生成的 React 钩子,而不是status
标志,因为派生布尔值能够提供更多细节,而这些细节是单个status
标志无法提供的,因为多个状态可能在给定时间为真(例如isFetching
和isSuccess
)。
对于查询端点,RTK Query 在isLoading
和isFetching
之间保持语义上的区别,以便为提供的派生信息提供更大的灵活性。
isLoading
指的是查询对于给定钩子首次处于飞行状态。此时将没有数据可用。isFetching
指的是查询对于给定端点 + 查询参数组合处于飞行状态,但不一定是首次。数据可能来自此钩子之前完成的早期请求,可能是使用之前的查询参数。
这种区别允许在处理 UI 行为时获得更大的控制。例如,isLoading
可用于在首次加载时显示骨架,而isFetching
可用于在从第 1 页更改到第 2 页或数据失效并重新获取时将旧数据变灰。
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 中以半透明的方式显示数据以表示重新获取状态,您可以将data
与isFetching
结合使用来实现。但是,如果您还希望只显示与当前参数相对应的数据,则可以使用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
允许您以高性能的方式从查询结果中获取特定部分。使用此功能时,除非所选项目的底层数据发生变化,否则组件不会重新渲染。如果所选项目是较大集合中的一个元素,它将忽略对同一集合中元素的更改。
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
的整体返回值执行浅层相等性检查,以确定是否强制重新渲染。也就是说,如果返回对象的任何值更改了引用,它将触发重新渲染。如果在回调中创建并使用了一个新的数组/对象作为返回值,由于每次运行回调时都被识别为新项目,因此会阻碍性能优势。当有意提供一个空数组/对象时,为了避免每次运行回调时重新创建它,可以在组件外部声明一个空数组/对象,以保持稳定的引用。
// 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'),
)
示例:观察缓存行为
此示例演示了请求去重和缓存行为
- 第一个
Pokemon
组件挂载并立即获取“bulbasaur” - 一秒钟后,另一个
Pokemon
组件使用“bulbasaur”渲染- 请注意,它从未显示“加载中...”并且没有发生新的网络请求?它在这里使用缓存。
- 片刻之后,添加了“pikachu”的
Pokemon
组件,并发生了一个新的请求。 - 当您单击特定
Pokemon
的“重新获取”时,它将使用一个请求更新该Pokemon
的所有实例。
单击“添加 bulbasaur”按钮。您将观察到上面描述的相同行为,直到您单击其中一个组件上的“重新获取”按钮。