浏览文档目录
Agent docs/query/optimistic-rollback
recipe

实现可回滚的乐观更新

取消冲突重取、保存缓存快照、不可变写入草稿,并在失败时回滚、完成时重新验证。

View raw Markdown
CONTRACT

执行契约

适用场景
写入延迟明显且用户需要即时反馈,同时旧缓存可以准确快照和恢复。
不要使用
复杂权限、服务端排序或跨多列表规则无法在客户端准确重演时,只显示 pending 变量或等待响应。
前置检查
列出所有受影响 Query Key、快照类型、冲突响应、回滚策略和最终重新验证范围。
验证结果
覆盖成功、网络失败、验证错误、409 冲突和并发提交;每条路径最终与服务端一致且无旧 refetch 覆盖新值。
失败模式
闪回旧值通常是 onMutate 未先等待 cancelQueries;错误后残留草稿说明快照缺失、原地修改或回滚 Key 不一致。
安全边界
乐观 UI 不证明服务端写入或授权成功;权限拒绝和业务冲突必须回滚,敏感动作不应伪装为已完成。
01

先决定是否需要改缓存

单一界面可直接用 mutation.variables 与 isPending 显示临时项,这是默认的低风险方案。只有多个缓存消费者必须立即同步时才使用 onMutate。列出精确 Query Key、旧值类型、服务端失败语义和最终重新验证范围。

02

事务式实现

onMutate 必须等待 cancelQueries 后再取快照和写新对象。onError 只在快照存在时恢复;onSettled 返回 invalidateQueries Promise,使 pending 覆盖完整重新验证周期。不要原地修改旧缓存。

src/features/issues/mutations.tsts
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) }),
})
03

并发与权限

同一 mutationKey 可能有多个 pending 变量,用 submittedAt 区分临时项。服务端响应才代表持久化与授权成功;409 冲突、验证失败和权限拒绝都必须回滚或重新获取,不能让乐观缓存继续冒充事实。

PRIMARY SOURCEShttps://tanstack.com/query/v5/docs/framework/react/guides/optimistic-updateshttps://tanstack.com/query/v5/docs/framework/react/guides/mutations