浏览文档目录
第 17 课 · 乐观写入

让乐观更新可以安全回滚

先选择 UI 级临时结果或缓存级更新,再用取消、快照、回滚和重新验证处理失败与并发。

TanStack Query生产25 分钟REV 01Markdown .md
完成后你将能够

能实现失败后恢复旧值、成功后以服务端为准,并能解释并发 Mutation 的显示策略。

01

先选择最小的乐观范围

如果临时结果只需显示在 Mutation 所在组件,用 variables 与 isPending 渲染半透明项目最简单,不会改缓存,也不需要回滚。多个界面都必须立刻看到变化时才直接更新缓存;跨组件可用 mutationKey 与 useMutationState 读取正在提交的变量。

02

缓存更新需要四步事务

onMutate 先取消会覆盖乐观值的相关重取,再保存旧值并不可变地写入草稿;返回的快照供 onError 回滚。onSettled 返回 invalidateQueries 的 Promise,让 Mutation 保持 pending,直到服务端事实重新进入缓存。

src/features/issues/useRenameIssue.tstsx
import { useMutation } from '@tanstack/react-query'

export function useRenameIssue(issueId: string) {
  const key = ['issues', 'detail', issueId] as const
  return useMutation({
    mutationFn: (title: string) => renameIssue({ id: issueId, title }),
    onMutate: async (title, context) => {
      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(key, result.previous)
    },
    onSettled: (_data, _error, _title, _result, context) =>
      context.client.invalidateQueries({ queryKey: key }),
  })
}
03

并发时不要假装只有一次写入

同一 mutationKey 可能同时存在多次提交,useMutationState 返回数组;使用 submittedAt 作为临时标识。复杂列表过滤、排序或权限规则若难以在客户端精确重演,应缩小乐观范围或只显示 pending 项,最终仍由服务端响应与失效重新对齐。

实践

把本课变成可验证的能力

完成检查点

乐观值在请求期间可见,失败恢复准确旧值,完成后缓存重新与服务端事实一致。

动手练习

实现 Issue 标题乐观重命名,注入一次 409 失败并同时提交两次修改,观察回滚与并发临时状态。

验证方法

分别测试成功、网络失败、业务冲突和并发提交;每条路径最终都应与服务端一致,Mutation pending 持续到重新验证完成。

常见错误
  • 未取消进行中的 refetch,导致旧响应覆盖乐观值。
  • 原地修改缓存对象,破坏订阅更新与回滚快照。
SOURCE REFERENCES · CHECKED 2026-08-03https://tanstack.com/query/v5/docs/framework/react/guides/optimistic-updateshttps://tanstack.com/query/v5/docs/framework/react/guides/mutations