Vue Query bietet zwei Möglichkeiten, Ihre Benutzeroberfläche optimistisch zu aktualisieren, bevor eine Mutation abgeschlossen ist. Sie können entweder die Option onMutate verwenden, um Ihren Cache direkt zu aktualisieren, oder die zurückgegebenen Variablen nutzen, um Ihre Benutzeroberfläche aus dem useMutation-Ergebnis zu aktualisieren.
Dies ist die einfachere Variante, da sie nicht direkt mit dem Cache interagiert.
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
Sie haben dann Zugriff auf addTodoMutation.variables, die das hinzugefügte Todo 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
<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>
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
{
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>
)
}
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 den dedizierten useMutationState Hook. Er lässt sich am besten mit einem mutationKey kombinieren
// 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 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 onMutate Handler-Option von useMutation, einen Wert zurückzugeben, der später sowohl an die onError als auch an die onSettled Handler als letztes Argument übergeben wird. In den meisten Fällen ist es am nützlichsten, eine Rollback-Funktion zu übergeben.
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] }),
})
Sie können auch die onSettled Funktion anstelle der separaten onError und onSuccess Handler verwenden, wenn Sie möchten
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
}
},
})
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.
Sehen Sie sich die Community-Ressourcen für eine Anleitung zu concurrent optimistic updates an.