---
id: optimistic-updates
track: learn
product: query
locale: zh-CN
order: 17
revision: 1
sourceCheckedOn: "2026-08-03"
versionRange: "Query ^5"
verifiedAgainst: "@tanstack/react-query@5.101.4"
contentModel: 2
prerequisites:
  - "理解 Mutation 生命周期与缓存失效"
  - "写入接口返回稳定实体或可重新获取"
sourceRefs:
  - "https://tanstack.com/query/v5/docs/framework/react/guides/optimistic-updates"
  - "https://tanstack.com/query/v5/docs/framework/react/guides/mutations"
---

# 让乐观更新可以安全回滚

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

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

## 先选择最小的乐观范围

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

## 缓存更新需要四步事务

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

File: `src/features/issues/useRenameIssue.ts`

```tsx
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 }),
  })
}
```

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

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

## 实践与验证

### 完成检查点

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

### 动手练习

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

### 验证方法

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

### 常见错误

- 未取消进行中的 refetch，导致旧响应覆盖乐观值。
- 原地修改缓存对象，破坏订阅更新与回滚快照。

## Official sources

- https://tanstack.com/query/v5/docs/framework/react/guides/optimistic-updates
- https://tanstack.com/query/v5/docs/framework/react/guides/mutations
