执行契约
- 适用场景
- 写入延迟明显且用户需要即时反馈,同时旧缓存可以准确快照和恢复。
- 不要使用
- 复杂权限、服务端排序或跨多列表规则无法在客户端准确重演时,只显示 pending 变量或等待响应。
- 前置检查
- 列出所有受影响 Query Key、快照类型、冲突响应、回滚策略和最终重新验证范围。
- 验证结果
- 覆盖成功、网络失败、验证错误、409 冲突和并发提交;每条路径最终与服务端一致且无旧 refetch 覆盖新值。
- 失败模式
- 闪回旧值通常是 onMutate 未先等待 cancelQueries;错误后残留草稿说明快照缺失、原地修改或回滚 Key 不一致。
- 安全边界
- 乐观 UI 不证明服务端写入或授权成功;权限拒绝和业务冲突必须回滚,敏感动作不应伪装为已完成。
先决定是否需要改缓存
单一界面可直接用 mutation.variables 与 isPending 显示临时项,这是默认的低风险方案。只有多个缓存消费者必须立即同步时才使用 onMutate。列出精确 Query Key、旧值类型、服务端失败语义和最终重新验证范围。
事务式实现
onMutate 必须等待 cancelQueries 后再取快照和写新对象。onError 只在快照存在时恢复;onSettled 返回 invalidateQueries Promise,使 pending 覆盖完整重新验证周期。不要原地修改旧缓存。
import { mutationOptions } from '@tanstack/react-query'
const issueKey = (id: string) => ['issues', 'detail', id] as const
export const renameIssueOptions = (issueId: string) => mutationOptions({
mutationFn: (title: string) => renameIssue({ id: issueId, title }),
onMutate: async (title, context) => {
const key = issueKey(issueId)
await context.client.cancelQueries({ queryKey: key })
const previous = context.client.getQueryData<Issue>(key)
context.client.setQueryData<Issue>(key, (issue) =>
issue ? { ...issue, title } : issue,
)
return { previous }
},
onError: (_error, _title, result, context) => {
if (result) context.client.setQueryData(issueKey(issueId), result.previous)
},
onSettled: (_data, _error, _title, _result, context) =>
context.client.invalidateQueries({ queryKey: issueKey(issueId) }),
})并发与权限
同一 mutationKey 可能有多个 pending 变量,用 submittedAt 区分临时项。服务端响应才代表持久化与授权成功;409 冲突、验证失败和权限拒绝都必须回滚或重新获取,不能让乐观缓存继续冒充事实。