缓存行为
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() {
// component subscribes to the data
const { data } = useGetUserQuery(1)
return <div>...</div>
}
function ComponentTwo() {
// component subscribes to the data
const { data } = useGetUserQuery(2)
return <div>...</div>
}
function ComponentThree() {
// component subscribes to the data
const { data } = useGetUserQuery(3)
return <div>...</div>
}
function ComponentFour() {
// component subscribes to the *same* data as ComponentThree,
// as it has the same query parameters
const { data } = useGetUserQuery(3)
return <div>...</div>
}
当四个组件订阅端点时,只有三个不同的端点+查询参数组合。查询参数 1
和 2
将分别只有一个订阅者,而查询参数 3
将有两个订阅者。RTK Query 将进行三个不同的获取;每个端点每个唯一的查询参数集一个。
只要至少有一个活跃的订阅者对该端点 + 参数组合感兴趣,数据就会保存在缓存中。当订阅者引用计数达到零时,会设置一个计时器,如果在计时器到期之前没有新的订阅该数据,缓存的数据将被删除。默认过期时间为 60 秒,可以在 整个 API 定义 和 每个端点 上进行配置。
在上面的示例中,如果 'ComponentThree' 被卸载,无论经过多长时间,数据都会保留在缓存中,因为 'ComponentFour' 仍然订阅了相同的数据,并且订阅引用计数将为 1
。但是,一旦 'ComponentFour' 被卸载,订阅者引用计数将为 0
。数据将在剩余的过期时间内保留在缓存中。如果在计时器到期之前没有创建新的订阅,缓存的数据最终将被删除。
操作缓存行为
除了默认行为之外,RTK Query 还提供了一些方法,可以在数据应该被视为无效或在其他情况下被认为适合“刷新”的情况下,更早地重新获取数据。
使用 keepUnusedDataFor
减少订阅时间
如上所述,在 默认缓存行为 和 缓存生命周期和订阅示例 中,默认情况下,数据将在订阅者引用计数达到零后保留在缓存中 60 秒。
可以使用 keepUnusedDataFor
选项为 API 定义和每个端点配置此值。请注意,如果提供了每个端点的版本,它将覆盖 API 定义上的设置。
将 keepUnusedDataFor
的值作为以秒为单位的数字提供,指定在订阅者引用计数达到零后数据应在缓存中保留多长时间。
- TypeScript
- JavaScript
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { Post } from './types'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
// global configuration for the api
keepUnusedDataFor: 30,
endpoints: (builder) => ({
getPosts: builder.query<Post[], number>({
query: () => `posts`,
// configuration for an individual endpoint, overriding the api setting
keepUnusedDataFor: 5,
}),
}),
})
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
// global configuration for the api
keepUnusedDataFor: 30,
endpoints: (builder) => ({
getPosts: builder.query({
query: () => `posts`,
// configuration for an individual endpoint, overriding the api setting
keepUnusedDataFor: 5,
}),
}),
})
使用 refetch
/initiate
按需重新获取
为了实现对重新获取数据的完全细粒度控制,可以使用从 useQuery
或 useQuerySubscription
钩子返回的结果属性中返回的 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() {
// force re-fetches the data
refetch()
}
function handleRefetchTwo() {
// has the same effect as `refetch` for the associated query
dispatch(
api.endpoints.getPosts.initiate(
{ count: 5 },
{ subscribe: false, forceRefetch: true },
),
)
}
return (
<div>
<button onClick={handleRefetchOne}>Force re-fetch 1</button>
<button onClick={handleRefetchTwo}>Force re-fetch 2</button>
</div>
)
}
使用refetchOnMountOrArgChange
鼓励重新获取
可以通过refetchOnMountOrArgChange
属性鼓励查询比平时更频繁地重新获取。这可以传递给整个端点,传递给单个钩子调用,或者在分派initiate
操作时传递(操作创建器选项的名称是forceRefetch
)。
refetchOnMountOrArgChange
用于在默认行为会提供缓存数据的情况下鼓励在其他情况下重新获取。
refetchOnMountOrArgChange
接受布尔值或以秒为单位的时间数字。
为此属性传递false
(默认值)将使用上面描述的默认行为。
为此属性传递true
将导致端点在查询的新订阅者添加时始终重新获取。如果传递给单个钩子调用而不是 api 定义本身,则这仅适用于该钩子调用。即,当调用钩子的组件挂载或参数更改时,它将始终重新获取,而不管端点 + 参数组合的缓存数据是否存在。
传递以秒为单位的number
作为值将使用以下行为
- 在创建查询订阅时
- 如果缓存中存在现有查询,它将比较当前时间与该查询的最后完成时间戳,
- 如果提供的秒数已过,它将重新获取。
- 如果没有查询,它将获取数据。
- 如果存在现有查询,但自上次查询以来指定的时间量尚未过去,它将提供现有的缓存数据。
- TypeScript
- JavaScript
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { Post } from './types'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
// global configuration for the api
refetchOnMountOrArgChange: 30,
endpoints: (builder) => ({
getPosts: builder.query<Post[], number>({
query: () => `posts`,
}),
}),
})
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
// global configuration for the api
refetchOnMountOrArgChange: 30,
endpoints: (builder) => ({
getPosts: builder.query({
query: () => `posts`,
}),
}),
})
import { useGetPostsQuery } from './api'
const Component = () => {
const { data } = useGetPostsQuery(
{ count: 5 },
// this overrules the api definition setting,
// forcing the query to always fetch when this component is mounted
{ refetchOnMountOrArgChange: true },
)
return <div>...</div>
}
使用refetchOnFocus
在窗口获得焦点时重新获取
refetchOnFocus
选项允许您控制 RTK Query 是否尝试在应用程序窗口重新获得焦点后重新获取所有已订阅的查询。
如果您在skip: true
旁边指定此选项,则在 skip 为 false 之前不会评估它。
请注意,这需要先调用 setupListeners
。
此选项在使用 createApi
的 API 定义和 useQuery
、useQuerySubscription
、useLazyQuery
和 useLazyQuerySubscription
钩子中都可用。
- TypeScript
- JavaScript
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { Post } from './types'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
// global configuration for the api
refetchOnFocus: true,
endpoints: (builder) => ({
getPosts: builder.query<Post[], number>({
query: () => `posts`,
}),
}),
})
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
// global configuration for the api
refetchOnFocus: true,
endpoints: (builder) => ({
getPosts: builder.query({
query: () => `posts`,
}),
}),
})
- TypeScript
- JavaScript
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),
})
// enable listener behavior for the store
setupListeners(store.dispatch)
export type RootState = ReturnType<typeof store.getState>
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),
})
// enable listener behavior for the store
setupListeners(store.dispatch)
网络重新连接时重新获取数据(使用 refetchOnReconnect
)
在 createApi
上的 refetchOnReconnect
选项允许您控制 RTK Query 是否会在重新获得网络连接后尝试重新获取所有已订阅的查询。
如果您在 skip: true
旁边指定此选项,则在 skip
为 false 之前,不会评估此选项。
请注意,这需要先调用 setupListeners
。
此选项在使用 createApi
的 API 定义和 useQuery
、useQuerySubscription
、useLazyQuery
和 useLazyQuerySubscription
钩子中都可用。
- TypeScript
- JavaScript
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { Post } from './types'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
// global configuration for the api
refetchOnReconnect: true,
endpoints: (builder) => ({
getPosts: builder.query<Post[], number>({
query: () => `posts`,
}),
}),
})
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
// global configuration for the api
refetchOnReconnect: true,
endpoints: (builder) => ({
getPosts: builder.query({
query: () => `posts`,
}),
}),
})
- TypeScript
- JavaScript
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),
})
// enable listener behavior for the store
setupListeners(store.dispatch)
export type RootState = ReturnType<typeof store.getState>
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),
})
// enable listener behavior for the store
setupListeners(store.dispatch)
通过使缓存标签失效来重新获取数据
RTK Query 使用可选的 缓存标签 系统来自动重新获取受变异端点影响的数据的查询端点。
有关此概念的完整详细信息,请参阅 自动重新获取。
权衡取舍
没有规范化或去重缓存
RTK Query 故意不实现会对来自多个请求的相同项进行去重的缓存。这有几个原因。
- 完全规范化的跨查询共享缓存是一个很难解决的问题。
- 我们目前没有时间、资源或兴趣来解决这个问题。
- 在许多情况下,当数据失效时简单地重新获取数据效果很好,而且更容易理解。
- 至少,RTKQ 可以帮助解决“获取一些数据”的一般用例,这对很多人来说是一个很大的痛点。
例如,假设我们有一个包含 getTodos
和 getTodo
端点的 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
值如何相互作用的实时演示。演示中显示了 Subscriptions
和 Queries
(包括缓存数据),以便您可视化(请注意,这也可以在 Redux Devtools 扩展 中查看)。
安装了两个组件,每个组件都使用相同的端点查询 (useGetUsersQuery(2)
)。您将能够观察到,当切换关闭组件时,订阅者引用计数将减少。在切换关闭两个组件使订阅者引用计数达到零后,您将观察到 Queries
部分下的缓存数据将持续 5 秒(此演示中为端点提供的 keepUnusedDataFor
值)。如果订阅者引用计数在整个持续时间内保持为 0,则缓存数据将从存储中删除。