在 mutation 完成之前,React Query 提供两种方式来乐观更新您的 UI。您可以选择使用 onMutate 选项直接更新您的缓存,或者利用返回的 variables 从 useMutation 结果更新您的 UI。
这是更简单的变体,因为它不直接与缓存交互。
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 列表(query 渲染的地方),当 mutation 处于 isPending 状态时,您可以将另一个项目追加到列表中。
<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 处于 pending 状态,我们就会渲染一个带有不同 opacity 的临时项目。一旦它完成,该项目将自动不再渲染。如果重新获取成功,我们应该会在列表中看到该项目成为“正常项目”。
如果 mutation 发生错误,该项目也将消失。但如果需要,我们可以通过检查 mutation 的 isError 状态来继续显示它。variables 在 mutation 发生错误时不会被清除,所以我们仍然可以访问它们,甚至可以显示一个重试按钮。
{
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 位于同一个组件中,这种方法非常有效。但是,您还可以通过专用的 useMutationState Hook 访问其他组件中的所有 mutation。它最好与 mutationKey 结合使用。
// 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 有可能会失败。在大多数这些失败情况下,您只需触发对您的乐观查询的重新获取,以将它们恢复到其真实的服务器状态。但在某些情况下,重新获取可能无法正常工作,并且 mutation 错误可能表示某种类型的服务器问题,使得无法重新获取。在这种情况下,您可以选择回滚您的更新。
为此,useMutation 的 onMutate 处理程序选项允许您返回一个值,该值稍后将作为最后一个参数传递给 onError 和 onSettled 处理程序。在大多数情况下,传递回滚函数最有用。
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'] }),
})
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 函数来代替单独的 onError 和 onSuccess 处理程序。
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 是代码量最少且通常更容易理解的方法。例如,您根本不需要处理回滚。
但是,如果屏幕上有多个地方需要了解更新,直接操作缓存将自动为您处理这个问题。
请查看社区资源,了解有关 并发乐观更新 的指南。