Angular Query 提供了两种方法来在 mutation 完成前乐观地更新您的 UI。您可以使用 onMutate 选项直接更新您的缓存,或者利用返回的 variables 来从 injectMutation 的结果中更新您的 UI。
这是更简单的变体,因为它不直接与缓存交互。
addTodo = injectMutation(() => ({
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: async () => {
return await queryClient.invalidateQueries({ queryKey: ['todos'] })
},
}))
addTodo = injectMutation(() => ({
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: async () => {
return await queryClient.invalidateQueries({ queryKey: ['todos'] })
},
}))
然后您将能够访问 addTodo.variables,其中包含添加的 todo。在渲染查询的 UI 列表中,当 mutation isPending 时,您可以向列表中添加另一项。
@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 在 mutation 挂起时不同。一旦完成,该项将不再显示。鉴于 refetch 成功,我们应该在列表中看到该项作为“普通项”。
如果 mutation 出错,该项也会消失。但如果我们愿意,可以通过检查 mutation 的 isError 状态来继续显示它。variables 在 mutation 出错时不会被清除,因此我们仍然可以访问它们,甚至可能显示一个重试按钮。
@Component({
template: `
@if (addTodo.isError()) {
<li style="color: red">
{{ addTodo.variables() }}
<button (click)="addTodo.mutate(addTodo.variables())">Retry</button>
</li>
}
`,
})
class TodosComponent {}
@Component({
template: `
@if (addTodo.isError()) {
<li style="color: red">
{{ addTodo.variables() }}
<button (click)="addTodo.mutate(addTodo.variables())">Retry</button>
</li>
}
`,
})
class TodosComponent {}
这种方法在 mutation 和查询位于同一组件中时效果非常好。但是,您也可以通过专用的 injectMutationState 函数来访问其他组件中的所有 mutations。最好将其与 mutationKey 结合使用。
// somewhere in your app
addTodo = injectMutation(() => ({
mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
mutationKey: ['addTodo'],
}))
// access variables somewhere else
mutationState = injectMutationState<string>(() => ({
filters: { mutationKey: ['addTodo'], status: 'pending' },
select: (mutation) => mutation.state.variables,
}))
// somewhere in your app
addTodo = injectMutation(() => ({
mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
mutationKey: ['addTodo'],
}))
// access variables somewhere else
mutationState = injectMutationState<string>(() => ({
filters: { mutationKey: ['addTodo'], status: 'pending' },
select: (mutation) => mutation.state.variables,
}))
variables 将是一个 Array,因为可能同时有多个 mutations 正在运行。如果我们需要一个唯一的 key 来标识这些项,我们也可以选择 mutation.state.submittedAt。这将使同时显示并发乐观更新变得轻而易举。
当您在执行 mutation 之前乐观地更新状态时,mutation 有可能会失败。在大多数失败情况下,您只需触发对您的乐观查询的 refetch,即可将它们恢复到其真实的服务器状态。但在某些情况下,refetch 可能无法正常工作,mutation 错误可能代表某种服务器问题,导致无法 refetch。在这种情况下,您可以选择回滚您的更新。
为此,injectMutation 的 onMutate 处理程序选项允许您返回一个值,该值稍后将作为最后一个参数传递给 onError 和 onSettled 处理程序。在大多数情况下,传递一个回滚函数是最有用的。
queryClient = inject(QueryClient)
updateTodo = injectMutation(() => ({
mutationFn: updateTodo,
// When mutate is called:
onMutate: async (newTodo) => {
// Cancel any outgoing refetches
// (so they don't overwrite our optimistic update)
await this.queryClient.cancelQueries({ queryKey: ['todos'] })
// Snapshot the previous value
const previousTodos = client.getQueryData(['todos'])
// Optimistically update to the new value
this.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) => {
client.setQueryData(['todos'], context.previousTodos)
},
// Always refetch after error or success:
onSettled: () => {
this.queryClient.invalidateQueries({ queryKey: ['todos'] })
},
}))
queryClient = inject(QueryClient)
updateTodo = injectMutation(() => ({
mutationFn: updateTodo,
// When mutate is called:
onMutate: async (newTodo) => {
// Cancel any outgoing refetches
// (so they don't overwrite our optimistic update)
await this.queryClient.cancelQueries({ queryKey: ['todos'] })
// Snapshot the previous value
const previousTodos = client.getQueryData(['todos'])
// Optimistically update to the new value
this.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) => {
client.setQueryData(['todos'], context.previousTodos)
},
// Always refetch after error or success:
onSettled: () => {
this.queryClient.invalidateQueries({ queryKey: ['todos'] })
},
}))
queryClient = inject(QueryClient)
updateTodo = injectMutation(() => ({
mutationFn: updateTodo,
// When mutate is called:
onMutate: async (newTodo) => {
// Cancel any outgoing refetches
// (so they don't overwrite our optimistic update)
await this.queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] })
// Snapshot the previous value
const previousTodo = this.queryClient.getQueryData(['todos', newTodo.id])
// Optimistically update to the new value
this.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) => {
this.queryClient.setQueryData(
['todos', context.newTodo.id],
context.previousTodo,
)
},
// Always refetch after error or success:
onSettled: (newTodo) => {
this.queryClient.invalidateQueries({ queryKey: ['todos', newTodo.id] })
},
}))
queryClient = inject(QueryClient)
updateTodo = injectMutation(() => ({
mutationFn: updateTodo,
// When mutate is called:
onMutate: async (newTodo) => {
// Cancel any outgoing refetches
// (so they don't overwrite our optimistic update)
await this.queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] })
// Snapshot the previous value
const previousTodo = this.queryClient.getQueryData(['todos', newTodo.id])
// Optimistically update to the new value
this.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) => {
this.queryClient.setQueryData(
['todos', context.newTodo.id],
context.previousTodo,
)
},
// Always refetch after error or success:
onSettled: (newTodo) => {
this.queryClient.invalidateQueries({ queryKey: ['todos', newTodo.id] })
},
}))
如果您愿意,也可以使用 onSettled 函数来代替独立的 onError 和 onSuccess 处理程序。
injectMutation({
mutationFn: updateTodo,
// ...
onSettled: (newTodo, error, variables, context) => {
if (error) {
// do something
}
},
})
injectMutation({
mutationFn: updateTodo,
// ...
onSettled: (newTodo, error, variables, context) => {
if (error) {
// do something
}
},
})
如果您只需要在一个地方显示乐观结果,使用 variables 并直接更新 UI 是代码量最少且通常更容易理解的方法。例如,您根本不需要处理回滚。
但是,如果屏幕上有多个地方需要了解更新,直接操作缓存将自动为您处理这个问题。
请参阅社区资源,了解关于 并发乐观更新 的指南。