跳到主要内容

 

缓存行为

RTK Query 的一个关键特性是其对缓存数据的管理。当从服务器获取数据时,RTK Query 会将数据存储在 Redux store 中作为 '缓存'。当对同一数据进行额外的请求时,RTK Query 会提供现有的缓存数据,而不是向服务器发送额外的请求。

RTK Query 提供了一些概念和工具来操作缓存行为并根据您的需求进行调整。

默认缓存行为

在 RTK Query 中,缓存基于:

  • API 端点定义
  • 当组件从端点订阅数据时使用的序列化查询参数
  • 活动订阅引用计数

当开始一个订阅时,与端点一起使用的参数被序列化并内部存储为请求的 queryCacheKey。任何未来产生相同 queryCacheKey 的请求(即,使用相同参数调用,考虑到序列化)将被对原始请求进行去重,并将共享相同的数据和更新。即,执行相同请求的两个独立组件将使用相同的缓存数据。

当尝试一个请求时,如果数据已经存在于缓存中,那么将提供该数据,不会向服务器发送新的请求。否则,如果数据不存在于缓存中,那么将发送新的请求,并将返回的响应存储在缓存中。

订阅是引用计数的。请求相同端点+参数的额外订阅会增加引用计数。只要数据有一个活动的 '订阅'(例如,如果有一个调用端点的 useQuery 钩子的组件被挂载),那么数据将保留在缓存中。一旦订阅被移除(例如,当最后一个订阅数据的组件卸载时),在一段时间后(默认 60 秒),数据将从缓存中移除。过期时间可以通过 API 定义整体keepUnusedDataFor 属性进行配置,也可以在 每个端点 的基础上进行配置。

缓存生命周期和订阅示例

想象一个端点,它期望一个 id 作为查询参数,有 4 个组件挂载,它们都从这个相同的端点请求数据:

import { useGetUserQuery } from './api.ts'

function ComponentOne() {
// 组件订阅数据
const { data } = useGetUserQuery(1)

return <div>...</div>
}

function ComponentTwo() {
// 组件订阅数据
const { data } = useGetUserQuery(2)

return <div>...</div>
}

function ComponentThree() {
// 组件订阅数据
const { data } = useGetUserQuery(3)

return <div>...</div>
}

function ComponentFour() {
// 组件订阅与 ComponentThree *相同* 的数据,
// 因为它有相同的查询参数
const { data } = useGetUserQuery(3)

return <div>...</div>
}

虽然有四个组件订阅了端点,但只有三个不同的端点 + 查询参数组合。查询参数 12 各有一个订阅者,而查询参数 3 有两个订阅者。RTK Query 将进行三次不同的获取;每个端点的每一组唯一的查询参数都会进行一次。

只要至少有一个活动订阅者对该端点 + 参数组合感兴趣,数据就会保留在缓存中。当订阅者引用计数达到零时,会设置一个计时器,如果在计时器到期之前没有新的订阅到该数据,缓存数据将被移除。默认的过期时间是 60 秒,可以为 API 定义整体 配置,也可以在 每个端点 的基础上进行配置。

如果在上面的例子中 'ComponentThree' 被卸载,无论过去多长时间,由于 'ComponentFour' 仍然订阅了相同的数据,数据将保留在缓存中,订阅引用计数将为 1。然而,一旦 'ComponentFour' 卸载,订阅者引用计数将为 0。数据将在缓存中保留剩余的过期时间。如果在计时器到期之前没有创建新的订阅,缓存数据最终将被移除。

操作缓存行为

除了默认行为外,RTK Query 提供了一些方法在数据应被视为无效,或者被认为适合 '刷新' 的情况下提前重新获取数据。

使用 keepUnusedDataFor 减少订阅时间

如上文 默认缓存行为缓存生命周期和订阅示例 所述,默认情况下,当订阅者引用计数达到零后,数据将在缓存中保留 60 秒。

这个值可以使用 keepUnusedDataFor 选项为 API 定义以及每个端点进行配置。注意,如果提供了每个端点的版本,它将覆盖 API 定义上的设置。

keepUnusedDataFor 提供一个以秒为单位的数字值,指定在订阅者引用计数达到零后,数据应在缓存中保留多长时间。

keepUnusedDataFor 配置
// 文件: types.ts noEmit
export interface Post {
id: number
name: string
}

// 文件: api.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { Post } from './types'

export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
// api 的全局配置
keepUnusedDataFor: 30,
endpoints: (builder) => ({
getPosts: builder.query<Post[], number>({
query: () => `posts`,
// 单个端点的配置,覆盖 api 设置
keepUnusedDataFor: 5,
}),
}),
})

使用 refetch/initiate 按需重新获取

为了实现对重新获取数据的完全粒度控制,你可以使用从 useQueryuseQuerySubscription 钩子返回的结果属性中的 refetch 函数。

调用 refetch 函数将强制重新获取关联的查询。

或者,你可以为端点分派 initiate thunk 动作,将选项 forceRefetch: true 传递给 thunk 动作创建器以达到相同的效果。

强制重新获取示例
import { useDispatch } from 'react-redux'
import { useGetPostsQuery } from './api'

const Component = () => {
const dispatch = useDispatch()
const { data, refetch } = useGetPostsQuery({ count: 5 })

function handleRefetchOne() {
// 强制重新获取数据
refetch()
}

function handleRefetchTwo() {
// 与 `refetch` 对关联查询有相同的效果
dispatch(
api.endpoints.getPosts.initiate(
{ count: 5 },
{ subscribe: false, forceRefetch: true },
),
)
}

return (
<div>
<button onClick={handleRefetchOne}>强制重新获取 1</button>
<button onClick={handleRefetchTwo}>强制重新获取 2</button>
</div>
)
}

通过 refetchOnMountOrArgChange 鼓励重新获取

通过 refetchOnMountOrArgChange 属性,查询可以被鼓励比通常更频繁地重新获取。这可以传递给整个端点,传递给单个钩子调用,或者在分派 initiate 动作时(动作创建器的选项名为 forceRefetch)。

refetchOnMountOrArgChange 用于在默认行为将提供缓存数据的额外情况下鼓励重新获取。

refetchOnMountOrArgChange 接受布尔值,或者作为时间(秒)的数字。

传递 false(默认值)对于此属性将使用上述描述的默认行为。

传递 true 对于此属性将导致端点在添加新的查询订阅者时总是重新获取。如果传递给单个钩子调用而不是 api 定义本身,那么这只适用于那个钩子调用。即,当调用钩子的组件挂载,或者参数改变时,它将总是重新获取,无论端点 + arg 组合的缓存数据是否已经存在。

传递一个 number 作为秒数的值将使用以下行为:

  • 在创建查询订阅时:
    • 如果缓存中存在现有查询,它将比较当前时间与该查询的最后满足时间戳,
    • 如果提供的时间(秒)已经过去,它将重新获取。
  • 如果没有查询,它将获取数据。
  • 如果存在现有查询,但自上次查询以来指定的时间还没有过去,它将提供现有的缓存数据。
配置如果数据超过给定时间则在订阅时重新获取
// 文件: types.ts noEmit
export interface Post {
id: number
name: string
}

// 文件: api.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { Post } from './types'

export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
// api 的全局配置
refetchOnMountOrArgChange: 30,
endpoints: (builder) => ({
getPosts: builder.query<Post[], number>({
query: () => `posts`,
}),
}),
})
强制在组件挂载时重新获取
import { useGetPostsQuery } from './api'

const Component = () => {
const { data } = useGetPostsQuery(
{ count: 5 },
// 这将覆盖 api 定义设置,
// 强制此组件挂载时总是获取查询
{ refetchOnMountOrArgChange: true },
)

return <div>...</div>
}

使用 refetchOnFocus 在窗口聚焦时重新获取

refetchOnFocus 选项允许你控制应用窗口重新获得焦点后,RTK Query 是否尝试重新获取所有已订阅的查询。

如果你在 skip: true 旁边指定了此选项,那么在 skip 为 false 之前,不会对此进行评估。

请注意,这需要已经调用了 setupListeners

此选项在 createApi 的 api 定义以及 useQueryuseQuerySubscriptionuseLazyQueryuseLazyQuerySubscription 钩子上都可用。

src/services/api.ts
// 文件: src/services/types.ts noEmit
export interface Post {
id: number
name: string
}

// 文件: src/services/api.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { Post } from './types'

export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
// api 的全局配置
refetchOnFocus: true,
endpoints: (builder) => ({
getPosts: builder.query<Post[], number>({
query: () => `posts`,
}),
}),
})
src/store.ts
// 文件: src/services/types.ts noEmit
export interface Post {
id: number
name: string
}

// 文件: src/services/api.ts noEmit
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { Post } from './types'

export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
// api 的全局配置
refetchOnFocus: true,
endpoints: (builder) => ({
getPosts: builder.query<Post[], number>({
query: () => `posts`,
}),
}),
})

// 文件: src/store.ts
import { configureStore } from '@reduxjs/toolkit'
import { setupListeners } from '@reduxjs/toolkit/query'
import { api } from './services/api'

export const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer,
},
middleware: (gDM) => gDM().concat(api.middleware),
})

// 为 store 启用监听器行为
setupListeners(store.dispatch)

export type RootState = ReturnType<typeof store.getState>

使用 refetchOnReconnect 在网络重新连接时重新获取

createApi 上的 refetchOnReconnect 选项允许你控制在重新获得网络连接后,RTK Query 是否尝试重新获取所有已订阅的查询。

如果你在 skip: true 旁边指定了此选项,那么在 skip 为 false 之前,不会对此进行评估

请注意,这需要已经调用了 setupListeners

此选项在 createApi 的 api 定义以及 useQueryuseQuerySubscriptionuseLazyQueryuseLazyQuerySubscription 钩子上都可用。

src/services/api.ts
// 文件: src/services/types.ts noEmit
export interface Post {
id: number
name: string
}

// 文件: src/services/api.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { Post } from './types'

export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
// api 的全局配置
refetchOnReconnect: true,
endpoints: (builder) => ({
getPosts: builder.query<Post[], number>({
query: () => `posts`,
}),
}),
})
src/store.ts
// 文件: src/services/types.ts noEmit
export interface Post {
id: number
name: string
}

// 文件: src/services/api.ts noEmit
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { Post } from './types'

export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
// api 的全局配置
refetchOnReconnect: true,
endpoints: (builder) => ({
getPosts: builder.query<Post[], number>({
query: () => `posts`,
}),
}),
})

// 文件: src/store.ts
import { configureStore } from '@reduxjs/toolkit'
import { setupListeners } from '@reduxjs/toolkit/query'
import { api } from './services/api'

export const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer,
},
middleware: (gDM) => gDM().concat(api.middleware),
})

// 为 store 启用监听器行为
setupListeners(store.dispatch)

export type RootState = ReturnType<typeof store.getState>

通过使缓存标签无效来在突变后重新获取

RTK Query 使用一个可选的缓存标签系统,以自动化那些数据受到突变端点影响的查询端点的重新获取。

有关此概念的完整细节,请参见自动重新获取

权衡

没有规范化或去重的缓存

RTK Query 故意不实现一个可以在多个请求之间去重相同项目的缓存。这有几个原因:

  • 一个完全规范化的跨查询共享缓存是一个难以解决的问题
  • 我们现在没有时间、资源或兴趣尝试解决这个问题
  • 在许多情况下,当数据无效时简单地重新获取数据效果很好,而且更容易理解
  • 至少,RTKQ 可以帮助解决"获取一些数据"的一般用例,这对很多人来说是一个大痛点

举个例子,假设我们有一个带有 getTodosgetTodo 端点的 API 切片,我们的组件进行以下查询:

  • getTodos()
  • getTodos({filter: 'odd'})
  • getTodo({id: 1})

每个查询结果都会包含一个看起来像 {id: 1} 的 Todo 对象。

在一个完全规范化的去重缓存中,只会存储这个 Todo 对象的一个副本。然而,RTK Query 在缓存中独立保存每个查询结果。所以,这会导致在 Redux 存储中缓存三个独立的 Todo 副本。然而,如果所有的端点都一致地提供相同的标签(如 {type: 'Todo', id: 1}),那么使该标签无效将强制所有匹配的端点重新获取他们的数据以保持一致性。

Redux 文档一直推荐在规范化的查找表中保持数据,以便轻松地通过 ID 找到项目并在存储中更新它们,而 RTK 的 createEntityAdapter 被设计用来帮助管理规范化的状态。这些概念仍然有价值,不会消失。然而,如果你正在使用 RTK Query 来管理缓存数据,那么你自己操作数据的需求就会减少。

这里还有一些可以帮助的额外点:

  • 生成的查询钩子有一个 selectFromResult 选项,允许组件从查询结果中读取单个数据片段。例如,一个 <TodoList> 组件可能会调用 useTodosQuery(),每个单独的 <TodoListItem> 可以使用相同的查询钩子,但从结果中选择以获取正确的 todo 对象。
  • 你可以使用 transformResponse 端点选项 来修改获取的数据,使其以不同的形状存储,例如使用 createEntityAdapter 在数据插入缓存之前规范化这一响应的数据。

更多信息

示例

缓存订阅生命周期演示

这个示例是一个实时演示,展示了订阅者引用计数和 keepUnusedDataFor 的值如何相互作用。在演示中,SubscriptionsQueries(包括缓存的数据)都会显示出来,供你可视化(注意,这也可以在 Redux Devtools Extension 中查看)。

挂载了两个组件,每个组件都有相同的端点查询(useGetUsersQuery(2))。你将能够观察到,当切换组件时,订阅者引用计数会减少。当切换关闭所有组件,使得订阅者引用计数达到零时,你会观察到 Queries 部分下的缓存数据将持续 5 秒(这是在此演示中为端点提供的 keepUnusedDataFor 的值)。如果订阅者引用计数在整个期间保持为 0,那么缓存的数据将从存储中移除。