乐观更新

React Query 提供了两种方式,可以在变更(mutation)完成之前乐观地更新你的 UI。你可以使用 onMutate 选项直接更新缓存,或者利用返回的 variablesuseMutation 的结果中更新 UI。

通过 UI 更新

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

const addTodoMutation = useMutation({
  mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
  // 确保 _返回_ 查询失效(invalidation)的 Promise
  // 这样可以使变更在重新获取完成前保持在 `pending` 状态
  onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
})

const { isPending, submittedAt, variables, mutate, isError } = addTodoMutation

之后,你就可以访问 addTodoMutation.variables,它包含了新添加的待办事项。在渲染查询结果的 UI 列表中,你可以在变更 isPending 时向列表追加一个新项目:

<ul>
  {todoQuery.items.map((todo) => (
    <li key={todo.id}>{todo.text}</li>
  ))}
  {isPending && <li style={{ opacity: 0.5 }}>{variables}</li>}
</ul>

只要变更处于待处理状态,我们就会渲染一个透明度不同的临时项目。一旦变更完成,该项目将自动不再渲染。如果重新获取成功,我们应该能在列表中看到这个项目作为“正常项目”出现。

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

{
  isError && (
    <li style={{ color: 'red' }}>
      {variables}
      <button onClick={() => mutate(variables)}>重试</button>
    </li>
  )
}

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

如果变更和查询位于同一个组件中,这种方法效果很好。然而,你也可以通过专用的 useMutationState Hook 在其他组件中访问所有变更。最好结合 mutationKey 使用:

// 在应用的某个地方
const { mutate } = useMutation({
  mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
  onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
  mutationKey: ['addTodo'],
})

// 在其他地方访问 variables
const variables = useMutationState<string>({
  filters: { mutationKey: ['addTodo'], status: 'pending' },
  select: (mutation) => mutation.state.variables,
})

variables 将是一个 Array,因为可能同时有多个变更在运行。如果我们需要一个唯一的键来标识项目,我们也可以选择 mutation.state.submittedAt。这甚至能让并发的乐观更新变得轻而易举。

通过缓存更新

当你在执行变更之前乐观地更新状态时,变更有失败的可能。在大多数失败情况下,你可以简单地触发对乐观查询的重新获取,将其恢复到真实的服务器状态。但在某些情况下,重新获取可能无法正常工作,且变更错误可能代表某种服务器问题,导致无法重新获取。在这种情况下,你可以选择回滚你的更新。

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

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

const queryClient = useQueryClient()

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

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

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

    // 返回一个包含快照值的结果
    return { previousTodos }
  },
  // 如果变更失败,
  // 使用 onMutate 返回的结果进行回滚
  onError: (err, newTodo, onMutateResult, context) => {
    context.client.setQueryData(['todos'], onMutateResult.previousTodos)
  },
  // 无论成功或失败后都重新获取:
  onSettled: (data, error, variables, onMutateResult, context) =>
    context.client.invalidateQueries({ queryKey: ['todos'] }),
})

更新单个待办事项

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

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

    // 乐观地更新为新值
    context.client.setQueryData(['todos', newTodo.id], newTodo)

    // 返回一个包含先前和新待办事项的结果
    return { previousTodo, newTodo }
  },
  // 如果变更失败,使用上面返回的结果
  onError: (err, newTodo, onMutateResult, context) => {
    context.client.setQueryData(
      ['todos', onMutateResult.newTodo.id],
      onMutateResult.previousTodo,
    )
  },
  // 无论成功或失败后都重新获取:
  onSettled: (newTodo, error, variables, onMutateResult, context) =>
    context.client.invalidateQueries({ queryKey: ['todos', newTodo.id] }),
})

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

useMutation({
  mutationFn: updateTodo,
  // ...
  onSettled: async (newTodo, error, variables, onMutateResult, context) => {
    if (error) {
      // 做一些事情
    }
  },
})

何时使用哪种方式

如果乐观结果只需要在一个地方显示,使用 variables 并直接更新 UI 是代码量更少、逻辑更清晰的方法。例如,你完全不需要处理回滚。

然而,如果你屏幕上有多个位置需要知道这个更新,直接操作缓存会自动为你处理这些情况。

进一步阅读

查看社区资源,了解关于 React Query 中的并发乐观更新 的指南。

最后更新于

这有帮助吗?