手动缓存更新
概述
在大多数情况下,为了在后端触发更改后接收最新的数据,你可以利用缓存标签失效来执行自动重新获取。这将导致查询在被告知发生了会使其数据过时的变更后重新获取其数据。
我们建议在大多数情况下优先使用自动重新获取而不是手动缓存更新。
然而,_确实有_手动缓存更新必要的使用场景,例如"乐观"或"悲观"更新,或者作为缓存条目生命周期的一部分修改数据。
RTK Query为这些使用场景导出了thunks,附加到api.utils
:
updateQueryData
:更新已经存在的缓存条目upsertQueryData
:创建或替换缓存条目
由于这些是thunks,你可以在任何有访问dispatch
的地方分派它们。
更新现有缓存条目
对于现有缓存条目的更新,使用updateQueryData
。
updateQueryData
严格用于对现有缓存条目执行_更新_,而不是创建新条目。如果分派了一个updateQueryData
thunk操作,并且endpointName
+ args
组合不匹配任何现有的缓存条目,那么提供的recipe
回调将不会被调用,也不会返回任何patches
或inversePatches
。
手动更新缓存条目的使用场景:
- 当尝试进行变更时,向用户提供即时反馈
- 在变更后,更新已经缓存的大量项目列表中的单个项目,而不是重新获取整个列表
- 通过立即反馈,将大量的变更进行防抖处理,就像它们正在被应用一样,然后向服务器发送单个请求来更新防抖尝试
创建新的缓存条目或替换现有的
要创建或替换现有的缓存条目,使用upsertQueryData
。
upsertQueryData
旨在对现有的缓存条目执行_替换_或_创建_新的。由于upsertQueryData
无法访问缓存条目的先前状态,更新可能只能作为替换进行。相比之下,updateQueryData
允许对现有的缓存条目进行修补,但不能创建新的。
一个示例使用场景是悲观更新。如果客户端发起一个API调用来创建一个Post
,后端可能会返回其完整的数据,包括id
。然后我们可以使用upsertQueryData
为getPostById(id)
查询创建一个新的缓存条目,防止后续再次获取该项目。
配方
乐观更新
当你希望在触发mutation
后立即对缓存数据进行更新时,你可以应用一个乐观更新
。当你希望给用户的印象是他们的更改是即时的,即使变更请求仍在进行中,这可以是一个有用的模式。
乐观更新的核心概念是:
- 当你开始一个查询或变更,
onQueryStarted
将被执行 - 你通过分派
api.util.updateQueryData
在onQueryStarted
中手动更新缓存数据 - 然后,在
queryFulfilled
拒绝的情况下:- 你通过从早期分派得到的对象的
.undo
属性回滚它, 或者 - 你通过
api.util.invalidateTags
使缓存数据无效,以触发数据的完全重新获取
- 你通过从早期分派得到的对象的
在许多变更可能在短时间内被触发,导致请求重叠的情况下,如果尝试使用.undo
属性在失败时回滚补丁,你可能会遇到竞态条件。对于这些场景,最简单和最安全的方法通常是在错误时使标签无效,然后从服务器重新获取真正最新的数据。
// 文件: types.ts noEmit
export interface Post {
id: number
name: string
}
// 文件: api.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
import type { Post } from './types'
const api = createApi({
baseQuery: fetchBaseQuery({
baseUrl: '/',
}),
tagTypes: ['Post'],
endpoints: (build) => ({
getPost: build.query<Post, number>({
query: (id) => `post/${id}`,
providesTags: ['Post'],
}),
updatePost: build.mutation<void, Pick<Post, 'id'> & Partial<Post>>({
query: ({ id, ...patch }) => ({
url: `post/${id}`,
method: 'PATCH',
body: patch,
}),
async onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) {
const patchResult = dispatch(
api.util.updateQueryData('getPost', id, (draft) => {
Object.assign(draft, patch)
}),
)
try {
await queryFulfilled
} catch {
patchResult.undo()
/**
* 或者,失败时你可以使相应的缓存标签无效
* 来触发重新获取:
* dispatch(api.util.invalidateTags(['Post']))
*/
}
},
}),
}),
})
或者,如果你更喜欢稍微短一点的版本,使用.catch
- async onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) {
+ onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) {
const patchResult = dispatch(
api.util.updateQueryData('getPost', id, (draft) => {
Object.assign(draft, patch)
})
)
- try {
- await queryFulfilled
- } catch {
- patchResult.undo()
- }
+ queryFulfilled.catch(patchResult.undo)
}
示例
悲观更新
当你希望在触发 mutation
后,根据从服务器收到的响应来更新缓存数据时,你可以应用一个 悲观更新
。悲观更新
和 乐观更新
的区别在于,悲观更新
会等待服务器的响应后再更新缓存数据。
悲观更新的核心概念包括:
- 当你开始一个查询或变异时,
onQueryStarted
将被执行 - 你等待
queryFulfilled
解析为一个对象,该对象包含服务器返回的转换响应在data
属性中 - 你通过在
onQueryStarted
中分派api.util.updateQueryData
,使用服务器响应中的数据手动更新缓存数据 - 你通过在
onQueryStarted
中分派api.util.upsertQueryData
,使用后端返回的完整 Post 对象手动创建一个新的缓存条目。
// 文件: types.ts noEmit
export interface Post {
id: number
name: string
}
// 文件: api.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
import type { Post } from './types'
const api = createApi({
baseQuery: fetchBaseQuery({
baseUrl: '/',
}),
tagTypes: ['Post'],
endpoints: (build) => ({
getPost: build.query<Post, number>({
query: (id) => `post/${id}`,
providesTags: ['Post'],
}),
updatePost: build.mutation<Post, Pick<Post, 'id'> & Partial<Post>>({
query: ({ id, ...patch }) => ({
url: `post/${id}`,
method: 'PATCH',
body: patch,
}),
async onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) {
try {
const { data: updatedPost } = await queryFulfilled
const patchResult = dispatch(
api.util.updateQueryData('getPost', id, (draft) => {
Object.assign(draft, updatedPost)
}),
)
} catch {}
},
}),
createPost: build.mutation<Post, Pick<Post, 'id'> & Partial<Post>>({
query: ({ id, ...body }) => ({
url: `post/${id}`,
method: 'POST',
body,
}),
async onQueryStarted({ id }, { dispatch, queryFulfilled }) {
try {
const { data: createdPost } = await queryFulfilled
const patchResult = dispatch(
api.util.upsertQueryData('getPost', id, createdPost),
)
} catch {}
},
}),
}),
})
通用更新
如果你发现自己想在应用程序的其他地方更新缓存数据,只要你能访问到 store.dispatch
方法,包括在 React 组件中通过 useDispatch 钩子(或者对于 TypeScript 用户,如 useAppDispatch 的类型版本)就可以做到。
你应该通常避免在 onQueryStarted
回调之外的变异中手动更新缓存,除非有充分的理由,因为 RTK Query 的使用方式是将你的缓存数据视为服务器端状态的反映。
import { api } from './api'
import { useAppDispatch } from './store/hooks'
function App() {
const dispatch = useAppDispatch()
function handleClick() {
/**
* 这将更新对应于 `getPosts` 端点的查询的缓存数据,
* 当该端点使用无参数(undefined)时。
*/
const patchCollection = dispatch(
api.util.updateQueryData('getPosts', undefined, (draftPosts) => {
draftPosts.push({ id: 1, name: 'Teddy' })
}),
)
}
return <button onClick={handleClick}>添加帖子到缓存</button>
}