查询
概述
这是 RTK Query 的最常见用例。可以使用您选择的任何数据获取库执行查询操作,但一般建议只对检索数据的请求使用查询。对于在服务器上更改数据或可能使缓存失效的任何操作,您应使用 Mutation。
默认情况下,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,它应被编写为接受单个参数。如果您需要传入多个参数,请将它们格式化为单个 "options 对象"。
查询端点还可以在结果被缓存之前修改响应内容,定义 "tags" 来标识缓存失效,并提供缓存条目生命周期回调以在缓存条目被添加和删除时运行额外的逻辑。
当与 TypeScript 一起使用时,您应为返回类型和预期的查询参数提供泛型:build.query<ReturnType, ArgType>
。如果没有参数,使用 void
作为 arg 类型。
// 文件: types.ts noEmit
export interface Post {
id: number
name: string
}
// 文件: api.ts
// 或从 '@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) => ({
// 查询接受一个数字并返回一个 Post
getPost: build.query<Post, number>({
// 注意:可选的 `queryFn` 可以代替 `query` 使用
query: (id) => ({ url: `post/${id}` }),
// 选择数据并防止在钩子或选择器中嵌套属性
transformResponse: (response: { data: Post }, meta, arg) => response.data,
// 选择错误并防止在钩子或选择器中嵌套属性
transformErrorResponse: (
response: { status: string | number },
meta,
arg,
) => response.status,
providesTags: (result, error, id) => [{ type: 'Post', id }],
// 第二个参数是解构的 `QueryLifecycleApi`
async onQueryStarted(
arg,
{
dispatch,
getState,
extra,
requestId,
queryFulfilled,
getCacheEntry,
updateCachedData,
},
) {},
// 第二个参数是解构的 `QueryCacheLifecycleApi`
async onCacheEntryAdded(
arg,
{
dispatch,
getState,
extra,
requestId,
cacheEntryRemoved,
cacheDataLoaded,
getCacheEntry,
updateCachedData,
},
) {},
}),
}),
})
使用 React 钩子执行查询
如果您正在使用 React 钩子,RTK Query 为您做了一些额外的事情。主要的好处是您可以得到一个优化渲染的钩子,允 许您进行 '后台获取',以及为了方便提供 派生布尔值。
基于服务定义中的 endpoint
名称自动生成钩子。带有 getPost: builder.query()
的端点字段将生成一个名为 useGetPostQuery
的钩子。
钩子类型
有 5 个与查询相关的钩子:
useQuery
- 组合
useQuerySubscription
和useQueryState
,是主要的钩子。自动触发从端点获取数据的请求,'订阅'组件到缓存数据,并从 Redux 存储中读取请求状态和缓存数据。
- 组合
useQuerySubscription
- 返回一个
refetch
函数,并接受所有钩子选项。自动触发从端点获取数据的请求,并 '订阅' 组件到缓存数据。
- 返回一个
useQueryState
- 返回查询状态并接受
skip
和selectFromResult
。从 Redux 存储中读取请求状态和缓存数据。
- 返回查询状态并接受
useLazyQuery
- 返回一个包含
trigger
函数、查询结果和最后的 promise 信息的元组。类似于useQuery
,但可以手动控制数据获取的时机。注意:trigger
函数接受一个preferCacheValue?: boolean
的第二个参数,以便在已经存在缓存数据的情况下跳过请求。
- 返回一个包含
useLazyQuerySubscription
- 返回一个包含
trigger
函数和最后的 promise 信息的元组。类似于useQuerySubscription
,但可以手动控制数据获取的时机。注意:trigger
函数接受一个preferCacheValue?: boolean
的第二个参数,以便在已经存在缓存数据的情况下跳过请求。
- 返回一个包含
在实践中,标准的 useQuery
-based 钩子,如 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
- 当为真时,表示查询尚未开始。isLoading
- 当为真时,表示查询正在首次加载,还没有数据。对于首次触发的请求,这将为true
,但对于后续请求则不会。isFetching
- 当为真时,表示查询正在获取,但可能已经有了来自早期请求的数据。对于首次触发的请求以及后续请求,这都将为true
。isSuccess
- 当为真时,表示查询已经从成功的请求中获取到数据。isError
- 当为真时,表示查询处于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>加载中...</div>
if (!post) return <div>缺少帖子!</div>
return (
<div>
{post.name} {isFetching ? '...正在重新获取' : ''}
</div>
)
}
这个组件的设置方式有一些好的特性:
- 它只在初始加载时显示 '加载中...'
- 初始加载被定义为一个正在等待并且在缓存中没有数据的查询
- 当请求由轮询间隔重新触发时,它会在帖子名称后添加 '...正在重新 获取'
- 如果用户关闭了这个
PostDetail
,但在允许的时间内重新打开它,他们将立即得到一个缓存的结果,并且轮询将以之前的行为恢复。
查询加载状态
由createApi
的React特定版本自动生成的React钩子提供了反映给定查询当前状态的派生布尔值。相比于status
标志,生成的React钩子更倾向于使用派生布尔值,因为派生布尔值能够提供更多的细节,这是单一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>发生错误!</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>发生错误!</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}
/>
))
: '没有可用的数据'}
</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 }) {
// 将选择给定id的帖子,并且只有在给定帖子的数据发生变化时才会重新渲染
const { post } = api.useGetPostsQuery(undefined, {
selectFromResult: ({ data }) => ({
post: data?.find((post) => post.id === id),
}),
})
return <li>{post?.name}</li>
}
注意,对selectFromResult
的整体返回值执行浅等式检查,以确定是否强制重新渲染。即,如果返回的对象值中的任何一个改变了引用,它将触发重新渲染。如果在回调中创建并使用了新的数组/对象作为返回值,由于每次运行回调时都会被识别为新项,它将阻碍性能优势。当有意提供一个空数组/对象时,为了避免每次运行回调时都重新创建它,你可以在组件外部声明一个空数组/对象,以保持稳定的引用。
// 在这里声明的数组将保持稳定的引用,而不是再次被创建
const emptyArray: Post[] = []
function PostsList() {
// 此调用将导致初始渲染返回`posts`的空数组,
// 并在接收到数据时进行第二次渲染。
// 只有在`posts`数据发生变化时,它才会触发额外的重新渲染
const { posts } = api.useGetPostsQuery(undefined, {
selectFromResult: ({ data }) => ({
posts: data ?? emptyArray,
}),
})
return (
<ul>
{posts.map((post) => (
<PostById key={post.id} id={post.id} />
))}
</ul>
)
}
总结上述行为 - 返回的值必须正确地被记忆化。另请参见使用选择器派生数据和Redux基础 - RTK查询高级模式以获取更多信息。
避免不必要的请求
默认情况下,如果你添加了一个执行与现有查询相同的组件,将不会执行任何请求。
在某些情况下,你可能希望跳过这种行为并强制重新获取 - 在这种情况下,你可以调用由钩子返回的 refetch
。
如果你没有使用React Hooks,你可以像这样访问 refetch
:
const { status, data, error, refetch } = dispatch(
pokemonApi.endpoints.getPokemon.initiate('bulbasaur'),
)
示例:观察缓存行为
此示例演示了请求去重和缓存行为:
- 第一个
Pokemon
组件挂载并立即获取 'bulbasaur' - 一秒钟后,另一个 'bulbasaur' 的
Pokemon
组件被渲染- 注意到这个组件从未显示过 'Loading...' 并且没有新的网络请求发生吗?这里是使用了缓存。
- 此后的一刻,添加了一个 'pikachu' 的
Pokemon
组件,并发生 了新的请求。 - 当你点击特定
Pokemon
的 'Refetch',它将通过一个请求更新该Pokemon
的所有实例。
点击 'Add bulbasaur' 按钮。你将观察到上述描述的相同行为,直到你在其中一个组件上点击 'Refetch' 按钮。