Framework
Version

Optimistische Updates

Solid Query bietet zwei Möglichkeiten, Ihre Benutzeroberfläche optimistisch zu aktualisieren, bevor eine Mutation abgeschlossen ist. Sie können entweder die onMutate Option verwenden, um Ihren Cache direkt zu aktualisieren, oder die zurückgegebenen variables nutzen, um Ihre Benutzeroberfläche aus dem useMutation Ergebnis zu aktualisieren.

Über die Benutzeroberfläche

Dies ist die einfachere Variante, da sie nicht direkt mit dem Cache interagiert.

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

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

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>

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

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>
  )
}

Wenn die Mutation und die Abfrage nicht in derselben Komponente leben

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

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 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.

Über den Cache

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.

Aktualisieren einer Liste von Todos beim Hinzufügen eines neuen Todos

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'] }),
})

Aktualisieren eines einzelnen 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] }),
})

Sie können auch die onSettled Funktion anstelle der separaten onError und onSuccess Handler verwenden, wenn Sie möchten

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
    }
  },
})

Wann was zu verwenden ist

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.

Weiterführende Lektüre

Sehen Sie sich die Community-Ressourcen für eine Anleitung zu concurrent optimistic updates an.