框架
版本

乐观更新

Angular Query 提供了两种在变更完成之前乐观更新 UI 的方法。您可以使用 onMutate 选项直接更新缓存,也可以利用返回的 variablesinjectMutation 结果更新 UI。

通过 UI

这是更简单的方法,因为它不直接与缓存交互。

ts
addTodo = injectMutation(() => ({
  mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
  // 确保从查询失效中 _返回_ Promise
  // 以便变更在重新获取完成之前保持在 `pending` 状态
  onSettled: async () => {
    return await queryClient.invalidateQueries({ queryKey: ['todos'] })
  },
}))
addTodo = injectMutation(() => ({
  mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
  // 确保从查询失效中 _返回_ Promise
  // 以便变更在重新获取完成之前保持在 `pending` 状态
  onSettled: async () => {
    return await queryClient.invalidateQueries({ queryKey: ['todos'] })
  },
}))

然后您将可以访问 addTodo.variables,其中包含添加的待办事项。在您的 UI 列表中,渲染查询的位置,您可以在变更 isPending 时向列表追加另一个项目:

angular-ts
@Component({
  template: `
    @for (todo of todos.data(); track todo.id) {
      <li>{{ todo.title }}</li>
    }
    @if (addTodo.isPending()) {
      <li style="opacity: 0.5">{{ addTodo.variables() }}</li>
    }
  `,
})
class TodosComponent {}
@Component({
  template: `
    @for (todo of todos.data(); track todo.id) {
      <li>{{ todo.title }}</li>
    }
    @if (addTodo.isPending()) {
      <li style="opacity: 0.5">{{ addTodo.variables() }}</li>
    }
  `,
})
class TodosComponent {}

只要变更处于挂起状态,我们就会渲染一个具有不同 opacity 的临时项目。一旦完成,该项目将自动不再渲染。假设重新获取成功,我们应该会在列表中看到该项目为“正常项目”。

如果变更出错,该项目也会消失。但是,如果我们愿意,我们可以通过检查变更的 isError 状态来继续显示它。当变更出错时,variables _不会_被清除,因此我们仍然可以访问它们,甚至可以显示一个重试按钮:

angular-ts
@Component({
  template: `
    @if (addTodo.isError()) {
      <li style="color: red">
        {{ addTodo.variables() }}
        <button (click)="addTodo.mutate(addTodo.variables())">重试</button>
      </li>
    }
  `,
})
class TodosComponent {}
@Component({
  template: `
    @if (addTodo.isError()) {
      <li style="color: red">
        {{ addTodo.variables() }}
        <button (click)="addTodo.mutate(addTodo.variables())">重试</button>
      </li>
    }
  `,
})
class TodosComponent {}

如果变更和查询不在同一个组件中

如果变更和查询位于同一个组件中,则此方法效果很好。但是,您也可以通过专用的 injectMutationState 钩子访问其他组件中的所有变更。最好将其与 mutationKey 结合使用:

ts
// 在您的应用程序中的某个位置
addTodo = injectMutation(() => ({
  mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
  onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
  mutationKey: ['addTodo'],
}))

// 在其他地方访问变量

mutationState = injectMutationState<string>(() => ({
  filters: { mutationKey: ['addTodo'], status: 'pending' },
  select: (mutation) => mutation.state.variables,
}))
// 在您的应用程序中的某个位置
addTodo = injectMutation(() => ({
  mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
  onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
  mutationKey: ['addTodo'],
}))

// 在其他地方访问变量

mutationState = injectMutationState<string>(() => ({
  filters: { mutationKey: ['addTodo'], status: 'pending' },
  select: (mutation) => mutation.state.variables,
}))

variables 将是一个 Array,因为可能同时运行多个变更。如果我们需要项目的唯一键,我们还可以选择 mutation.state.submittedAt。这甚至可以轻松显示并发的乐观更新。

通过缓存

当您在执行变更之前乐观地更新状态时,变更可能会失败。在大多数这些失败情况下,您只需为乐观查询触发重新获取即可将其恢复到其真实的服务器状态。但是,在某些情况下,重新获取可能无法正常工作,并且变更错误可能表示某种服务器问题,从而无法重新获取。在这种情况下,您可以选择回滚更新。

为此,injectMutationonMutate 处理程序选项允许您返回一个值,该值稍后将作为最后一个参数传递给 onErroronSettled 处理程序。在大多数情况下,传递回滚函数最有用。

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

tsx
const queryClient = useQueryClient()

injectMutation({
  mutationFn: updateTodo,
  // 当调用 mutate 时:
  onMutate: async (newTodo) => {
    // 取消任何传出的重新获取
    // (这样它们就不会覆盖我们的乐观更新)
    await queryClient.cancelQueries({ queryKey: ['todos'] })

    // 快照先前的值
    const previousTodos = queryClient.getQueryData(['todos'])

    // 乐观地更新到新值
    queryClient.setQueryData(['todos'], (old) => [...old, newTodo])

    // 返回具有快照值的上下文对象
    return { previousTodos }
  },
  // 如果变更失败,
  // 使用从 onMutate 返回的上下文进行回滚
  onError: (err, newTodo, context) => {
    queryClient.setQueryData(['todos'], context.previousTodos)
  },
  // 始终在错误或成功后重新获取:
  onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
})
const queryClient = useQueryClient()

injectMutation({
  mutationFn: updateTodo,
  // 当调用 mutate 时:
  onMutate: async (newTodo) => {
    // 取消任何传出的重新获取
    // (这样它们就不会覆盖我们的乐观更新)
    await queryClient.cancelQueries({ queryKey: ['todos'] })

    // 快照先前的值
    const previousTodos = queryClient.getQueryData(['todos'])

    // 乐观地更新到新值
    queryClient.setQueryData(['todos'], (old) => [...old, newTodo])

    // 返回具有快照值的上下文对象
    return { previousTodos }
  },
  // 如果变更失败,
  // 使用从 onMutate 返回的上下文进行回滚
  onError: (err, newTodo, context) => {
    queryClient.setQueryData(['todos'], context.previousTodos)
  },
  // 始终在错误或成功后重新获取:
  onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
})

更新单个待办事项

tsx
injectMutation({
  mutationFn: updateTodo,
  // 当调用 mutate 时:
  onMutate: async (newTodo) => {
    // 取消任何传出的重新获取
    // (这样它们就不会覆盖我们的乐观更新)
    await queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] })

    // 快照先前的值
    const previousTodo = queryClient.getQueryData(['todos', newTodo.id])

    // 乐观地更新到新值
    queryClient.setQueryData(['todos', newTodo.id], newTodo)

    // 返回具有先前和新待办事项的上下文
    return { previousTodo, newTodo }
  },
  // 如果变更失败,请使用我们上面返回的上下文
  onError: (err, newTodo, context) => {
    queryClient.setQueryData(
      ['todos', context.newTodo.id],
      context.previousTodo,
    )
  },
  // 始终在错误或成功后重新获取:
  onSettled: (newTodo) =>
    queryClient.invalidateQueries({ queryKey: ['todos', newTodo.id] }),
})
injectMutation({
  mutationFn: updateTodo,
  // 当调用 mutate 时:
  onMutate: async (newTodo) => {
    // 取消任何传出的重新获取
    // (这样它们就不会覆盖我们的乐观更新)
    await queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] })

    // 快照先前的值
    const previousTodo = queryClient.getQueryData(['todos', newTodo.id])

    // 乐观地更新到新值
    queryClient.setQueryData(['todos', newTodo.id], newTodo)

    // 返回具有先前和新待办事项的上下文
    return { previousTodo, newTodo }
  },
  // 如果变更失败,请使用我们上面返回的上下文
  onError: (err, newTodo, context) => {
    queryClient.setQueryData(
      ['todos', context.newTodo.id],
      context.previousTodo,
    )
  },
  // 始终在错误或成功后重新获取:
  onSettled: (newTodo) =>
    queryClient.invalidateQueries({ queryKey: ['todos', newTodo.id] }),
})

如果您愿意,也可以使用 onSettled 函数代替单独的 onErroronSuccess 处理程序:

tsx
injectMutation({
  mutationFn: updateTodo,
  // ...
  onSettled: async (newTodo, error, variables, context) => {
    if (error) {
      // 执行某些操作
    }
  },
})
injectMutation({
  mutationFn: updateTodo,
  // ...
  onSettled: async (newTodo, error, variables, context) => {
    if (error) {
      // 执行某些操作
    }
  },
})

何时使用什么

如果您只有一个地方需要显示乐观结果,则使用 variables 并直接更新 UI 是代码量更少且通常更容易理解的方法。例如,您根本不需要处理回滚。

但是,如果屏幕上有多个地方需要了解更新,则直接操作缓存会自动为您处理此问题。