Angular Query bietet zwei Möglichkeiten, Ihre Benutzeroberfläche vor Abschluss einer Mutation optimistisch zu aktualisieren. Sie können entweder die Option onMutate verwenden, um Ihren Cache direkt zu aktualisieren, oder die zurückgegebenen variables nutzen, um Ihre Benutzeroberfläche aus dem injectMutation-Ergebnis zu aktualisieren.
Dies ist die einfachere Variante, da sie nicht direkt mit dem Cache interagiert.
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'] })
},
}))
Sie haben dann Zugriff auf addTodo.variables, die die hinzugefügte Aufgabe enthalten. In Ihrer UI-Liste, in der die Abfrage gerendert wird, können Sie ein weiteres Element zur Liste hinzufügen, während die Mutation isPending ist.
@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 {}
Wir rendern ein temporäres Element mit einer anderen opacity, solange die Mutation aussteht. Sobald sie abgeschlossen ist, wird das Element automatisch nicht mehr gerendert. Da der Refetch erfolgreich war, sollten wir das Element als "normales Element" in unserer Liste sehen.
Wenn die Mutation fehlschlägt, verschwindet das Element ebenfalls. Aber wir könnten es weiterhin anzeigen, wenn wir wollen, indem wir den isError Status der Mutation überprüfen. variables werden *nicht* gelöscht, wenn die Mutation fehlschlägt, sodass wir sie immer noch abrufen können, vielleicht sogar einen Wiederholungsschaltfläche anzeigen
@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 {}
Dieser Ansatz funktioniert sehr gut, wenn die Mutation und die Abfrage in derselben Komponente leben. Sie erhalten jedoch auch Zugriff auf alle Mutationen in anderen Komponenten über die dedizierte Funktion injectMutationState. Sie lässt sich am besten mit einem mutationKey kombinieren.
// 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 wird ein Array sein, da gleichzeitig mehrere Mutationen laufen könnten. Wenn wir einen eindeutigen Schlüssel für die Elemente benötigen, können wir auch mutation.state.submittedAt auswählen. Dies erleichtert sogar die Anzeige gleichzeitiger optimistischer Updates.
Wenn Sie Ihren Zustand optimistisch aktualisieren, bevor Sie eine Mutation durchführen, besteht die Möglichkeit, dass die Mutation fehlschlägt. In den meisten dieser Fehlerfälle können Sie einfach einen Refetch für Ihre optimistischen Abfragen auslösen, um sie auf ihren wahren Serverstatus zurückzusetzen. Unter bestimmten Umständen funktioniert das Refetchen möglicherweise nicht korrekt und der Mutationsfehler könnte eine Art Serverproblem darstellen, das kein Refetchen ermöglicht. In diesem Fall können Sie stattdessen wählen, Ihr Update zurückzusetzen.
Um dies zu tun, erlaubt die Option onMutate von injectMutation, einen Wert zurückzugeben, der später sowohl an die Handler onError als auch an onSettled als letztes Argument übergeben wird. In den meisten Fällen ist es am nützlichsten, eine Rollback-Funktion zu übergeben.
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] })
},
}))
Sie können auch die onSettled Funktion anstelle der separaten onError und onSuccess Handler verwenden, wenn Sie möchten
injectMutation({
mutationFn: updateTodo,
// ...
onSettled: (newTodo, error, variables, context) => {
if (error) {
// do something
}
},
})
injectMutation({
mutationFn: updateTodo,
// ...
onSettled: (newTodo, error, variables, context) => {
if (error) {
// do something
}
},
})
Wenn Sie nur einen Ort haben, an dem das optimistische Ergebnis angezeigt werden soll, ist die Verwendung von variables und die direkte Aktualisierung der Benutzeroberfläche der Ansatz, der am wenigsten Code erfordert und im Allgemeinen einfacher zu verstehen ist. Sie müssen zum Beispiel gar keine Rollbacks behandeln.
Wenn Sie jedoch mehrere Stellen auf dem Bildschirm haben, die über die Aktualisierung informiert werden müssten, kümmert sich die direkte Manipulation des Caches automatisch darum.
Schauen Sie sich die Community-Ressourcen für eine Anleitung zu Concurrent Optimistic Updates an.