框架
版本

乐观更新

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

通过 UI

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

tsx
const addTodoMutation = useMutation(() => {
  mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
  // make sure to _return_ the Promise from the query invalidation
  // so that the mutation stays in `pending` state until the refetch is finished
  onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
})

const { isPending, submittedAt, variables, mutate, isError } = addTodoMutation
const addTodoMutation = useMutation(() => {
  mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
  // make sure to _return_ the Promise from the query invalidation
  // so that the mutation stays in `pending` state until the refetch is finished
  onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
})

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

你将可以访问 addTodoMutation.variables,其中包含已添加的待办事项。在渲染了查询的 UI 列表中,你可以在 mutation isPending 时添加另一项。

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

我们渲染了一个临时项,在 mutation 挂起期间具有不同的 opacity。一旦完成,该项将不再渲染。鉴于 refetch 成功,我们应该在列表中看到该项是“正常项”。

如果 mutation 出错,该项也将消失。但如果需要,我们仍然可以继续显示它,方法是检查 mutation 的 isError 状态。 variables 在 mutation 出错时不会被清除,因此我们仍然可以访问它们,甚至可能显示重试按钮。

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

如果 mutation 和 query 不在同一个组件中

如果 mutation 和查询位于同一个组件中,此方法效果很好。但是,你也可以通过专用的 useMutationState hook 访问其他组件中的所有 mutation。最好将其与 mutationKey 结合使用。

tsx
// somewhere in your app
const { mutate } = useMutation(() => {
  mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
  onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
  mutationKey: ['addTodo'],
})

// access variables somewhere else
const variables = useMutationState<string>({
  filters: { mutationKey: ['addTodo'], status: 'pending' },
  select: (mutation) => mutation.state.variables,
})
// somewhere in your app
const { mutate } = useMutation(() => {
  mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
  onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
  mutationKey: ['addTodo'],
})

// access variables somewhere else
const variables = useMutationState<string>({
  filters: { mutationKey: ['addTodo'], status: 'pending' },
  select: (mutation) => mutation.state.variables,
})

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

通过缓存

当你在执行 mutation 之前乐观地更新你的状态时,mutation 有可能失败。在大多数此类失败情况下,你只需触发对你的乐观查询的 refetch,即可将它们恢复到其真实的服务器状态。但在某些情况下,refetch 可能无法正常工作,mutation 错误可能表示某种类型的服务器问题,导致无法 refetch。在这种情况下,你可以选择回滚你的更新。

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

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

tsx
const queryClient = useQueryClient()

useMutation(() => {
  mutationFn: updateTodo,
  // When mutate is called:
  onMutate: async (newTodo) => {
    // Cancel any outgoing refetches
    // (so they don't overwrite our optimistic update)
    await queryClient.cancelQueries({ queryKey: ['todos'] })

    // Snapshot the previous value
    const previousTodos = queryClient.getQueryData(['todos'])

    // Optimistically update to the new value
    queryClient.setQueryData(['todos'], (old) => [...old, newTodo])

    // Return a context object with the snapshotted value
    return { previousTodos }
  },
  // If the mutation fails,
  // use the context returned from onMutate to roll back
  onError: (err, newTodo, context) => {
    queryClient.setQueryData(['todos'], context.previousTodos)
  },
  // Always refetch after error or success:
  onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
})
const queryClient = useQueryClient()

useMutation(() => {
  mutationFn: updateTodo,
  // When mutate is called:
  onMutate: async (newTodo) => {
    // Cancel any outgoing refetches
    // (so they don't overwrite our optimistic update)
    await queryClient.cancelQueries({ queryKey: ['todos'] })

    // Snapshot the previous value
    const previousTodos = queryClient.getQueryData(['todos'])

    // Optimistically update to the new value
    queryClient.setQueryData(['todos'], (old) => [...old, newTodo])

    // Return a context object with the snapshotted value
    return { previousTodos }
  },
  // If the mutation fails,
  // use the context returned from onMutate to roll back
  onError: (err, newTodo, context) => {
    queryClient.setQueryData(['todos'], context.previousTodos)
  },
  // Always refetch after error or success:
  onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
})

更新单个待办事项

tsx
useMutation(() => {
  mutationFn: updateTodo,
  // When mutate is called:
  onMutate: async (newTodo) => {
    // Cancel any outgoing refetches
    // (so they don't overwrite our optimistic update)
    await queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] })

    // Snapshot the previous value
    const previousTodo = queryClient.getQueryData(['todos', newTodo.id])

    // Optimistically update to the new value
    queryClient.setQueryData(['todos', newTodo.id], newTodo)

    // Return a context with the previous and new todo
    return { previousTodo, newTodo }
  },
  // If the mutation fails, use the context we returned above
  onError: (err, newTodo, context) => {
    queryClient.setQueryData(
      ['todos', context.newTodo.id],
      context.previousTodo,
    )
  },
  // Always refetch after error or success:
  onSettled: (newTodo) =>
    queryClient.invalidateQueries({ queryKey: ['todos', newTodo.id] }),
})
useMutation(() => {
  mutationFn: updateTodo,
  // When mutate is called:
  onMutate: async (newTodo) => {
    // Cancel any outgoing refetches
    // (so they don't overwrite our optimistic update)
    await queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] })

    // Snapshot the previous value
    const previousTodo = queryClient.getQueryData(['todos', newTodo.id])

    // Optimistically update to the new value
    queryClient.setQueryData(['todos', newTodo.id], newTodo)

    // Return a context with the previous and new todo
    return { previousTodo, newTodo }
  },
  // If the mutation fails, use the context we returned above
  onError: (err, newTodo, context) => {
    queryClient.setQueryData(
      ['todos', context.newTodo.id],
      context.previousTodo,
    )
  },
  // Always refetch after error or success:
  onSettled: (newTodo) =>
    queryClient.invalidateQueries({ queryKey: ['todos', newTodo.id] }),
})

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

tsx
useMutation(() => {
  mutationFn: updateTodo,
  // ...
  onSettled: async (newTodo, error, variables, context) => {
    if (error) {
      // do something
    }
  },
})
useMutation(() => {
  mutationFn: updateTodo,
  // ...
  onSettled: async (newTodo, error, variables, context) => {
    if (error) {
      // do something
    }
  },
})

何时使用何种方式

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

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

延伸阅读

请查看社区资源,了解有关 并发乐观更新 的指南。