译零 TransZero · TanStack Query 中文文档
主站

乐观更新

更新于 2026-11-02 Angular 查看英文原文 ↗

Angular Query 提供两种方式,让你在修改(mutation)完成前对 UI 进行乐观更新。你可以使用 onMutate 选项直接更新缓存,也可以利用返回的 variables,基于 injectMutation 的结果来更新 UI。

通过 UI 实现

这是较简单的方式,不直接与缓存交互。

TypeScript
1addTodo = injectMutation(() => ({2  mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),3  // make sure to _return_ the Promise from the query invalidation4  // so that the mutation stays in `pending` state until the refetch is finished5  onSettled: async () => {6    return await queryClient.invalidateQueries({ queryKey: ['todos'] })7  },8}))

随后你将能访问 addTodo.variables,其中包含新添加的待办事项。在渲染该查询的 UI 列表中,你可在修改 isPending 期间向列表追加一项:

angular-ts
1@Component({2  template: `3    @for (todo of todos.data(); track todo.id) {4      <li>{{ todo.title }}</li>5    }6    @if (addTodo.isPending()) {7      <li style="opacity: 0.5">{{ addTodo.variables() }}</li>8    }9  `,10})11class TodosComponent {}

只要修改处于等待中(pending),我们就渲染一个临时项,并赋予其不同的 opacity。一旦修改完成,该项将自动不再渲染。若重新拉取(refetch)成功,你将在列表中看到该项变为“常规项”。

若修改出错,该项同样会消失。但如果你希望它继续显示,可检查修改的 isError 状态。variables 在修改出错时 不会 被清空,因此你仍可访问它们,甚至可能展示一个重试按钮:

angular-ts
1@Component({2  template: `3    @if (addTodo.isError()) {4      <li style="color: red">5        {{ addTodo.variables() }}6        <button (click)="addTodo.mutate(addTodo.variables())">Retry</button>7      </li>8    }9  `,10})11class TodosComponent {}

若修改与查询不在同一组件中

当修改与查询位于同一组件时,此方法效果很好。但你也可通过专用的 injectMutationState 函数,在其他组件中访问全部修改。它最适合与 mutationKey 结合使用:

TypeScript
1// somewhere in your app2addTodo = injectMutation(() => ({3  mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),4  onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),5  mutationKey: ['addTodo'],6}))7 8// access variables somewhere else9 10mutationState = injectMutationState<string>(() => ({11  filters: { mutationKey: ['addTodo'], status: 'pending' },12  select: (mutation) => mutation.state.variables,13}))

variables 将是一个 Array,因为可能同时运行多个修改。若需为各项指定唯一键,你还可以选择 mutation.state.submittedAt。这甚至能让并发乐观更新的展示变得轻而易举。

通过缓存实现

当你在执行修改前对状态进行乐观更新时,修改存在失败风险。在大多数失败情形下,你只需对相关乐观查询触发一次重新拉取(refetch),即可将其恢复为真实的服务器状态。但在某些情况下,重新拉取可能无法正确工作,且修改错误可能代表某种服务器问题,导致无法重新拉取。此时,你可选择回滚(rollback)你的更新。

为此,injectMutation 的 onMutate 处理器选项允许你返回一个值,该值后续将作为最后一个参数,传入 onError 和 onSettled 处理器中。多数情况下,传入一个回滚函数最为实用。

向待办事项列表添加新待办事项时更新列表

TypeScript
1queryClient = inject(QueryClient)2 3updateTodo = injectMutation(() => ({4  mutationFn: updateTodo,5  // When mutate is called:6  onMutate: async (newTodo, context) => {7    // Cancel any outgoing refetches8    // (so they don't overwrite our optimistic update)9    await context.client.cancelQueries({ queryKey: ['todos'] })10 11    // Snapshot the previous value12    const previousTodos = context.client.getQueryData(['todos'])13 14    // Optimistically update to the new value15    context.client.setQueryData(['todos'], (old) => [...old, newTodo])16 17    // Return a result object with the snapshotted value18    return { previousTodos }19  },20  // If the mutation fails,21  // use the result returned from onMutate to roll back22  onError: (err, newTodo, onMutateResult, context) => {23    context.client.setQueryData(['todos'], onMutateResult.previousTodos)24  },25  // Always refetch after error or success:26  onSettled: (data, error, variables, onMutateResult, context) => {27    context.client.invalidateQueries({ queryKey: ['todos'] })28  },29}))

更新单个待办事项

TypeScript
1queryClient = inject(QueryClient)2 3updateTodo = injectMutation(() => ({4  mutationFn: updateTodo,5  // When mutate is called:6  onMutate: async (newTodo, context) => {7    // Cancel any outgoing refetches8    // (so they don't overwrite our optimistic update)9    await context.client.cancelQueries({ queryKey: ['todos', newTodo.id] })10 11    // Snapshot the previous value12    const previousTodo = context.client.getQueryData(['todos', newTodo.id])13 14    // Optimistically update to the new value15    context.client.setQueryData(['todos', newTodo.id], newTodo)16 17    // Return a result with the previous and new todo18    return { previousTodo, newTodo }19  },20  // If the mutation fails, use the result we returned above21  onError: (err, newTodo, onMutateResult, context) => {22    context.client.setQueryData(23      ['todos', onMutateResult.newTodo.id],24      onMutateResult.previousTodo,25    )26  },27  // Always refetch after error or success:28  onSettled: (newTodo, error, variables, onMutateResult, context) => {29    context.client.invalidateQueries({ queryKey: ['todos', newTodo.id] })30  },31}))

你也可按需使用 onSettled 函数,替代单独的 onError 和 onSuccess 处理器:

TypeScript
1injectMutation({2  mutationFn: updateTodo,3  // ...4  onSettled: (newTodo, error, variables, onMutateResult, context) => {5    if (error) {6      // do something7    }8  },9})

何时选用哪种方式

若仅有一个位置需要显示乐观结果,则直接使用 variables 并更新 UI 是代码量更少、逻辑更清晰的方法。例如,你完全无需处理回滚。

然而,若屏幕上存在多个位置需要感知该更新,则直接操作缓存将自动为你处理所有这些位置。

延伸阅读

请参阅 TkDodo 撰写的指南:并发乐观更新。

本页为 TanStack Query 文档(Angular 版)的非官方中文翻译,原文由上游团队维护,遵循 MIT 许可。 译文如有偏差,以英文原文为准。

译自上游 59d11de