---
id: query.optimistic-rollback
kind: recipe
product: query
framework: react
locale: zh-CN
revision: 1
sourceCheckedOn: "2026-08-03"
versionRange: "^5"
verifiedAgainst: "@tanstack/react-query@5.101.4"
contentModel: 2
packages:
  - "@tanstack/react-query"
tasks:
  - "optimistic-update"
  - "rollback-mutation"
  - "reconcile-server-state"
sourceRefs:
  - "https://tanstack.com/query/v5/docs/framework/react/guides/optimistic-updates"
  - "https://tanstack.com/query/v5/docs/framework/react/guides/mutations"
---

# 实现可回滚的乐观更新

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

## 执行契约

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

## 先决定是否需要改缓存

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

## 事务式实现

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

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

```ts
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 冲突、验证失败和权限拒绝都必须回滚或重新获取，不能让乐观缓存继续冒充事实。

## Official sources

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