TanStack Query Mutations: Optimistic Updates und Cache-Invalidierung im Detail
AI generated
{ }
React 19 · TanStack Query · Server State
TanStack Query Mutations meistern
Optimistic Updates, Rollback und gezielte Cache-Invalidierung statt pauschalem Refetch

Ein POST-Request ist schnell abgeschickt, die eigentliche Arbeit beginnt danach: Cache aktualisieren, betroffene Queries invalidieren, bei einem Fehler sauber zuruecksetzen. useMutation bietet dafuer ein klares API-Muster statt verstreuter Handler-Logik.

14 Min. Lesezeit useMutation · TanStack Query v5 Cache Invalidierung

1. useQuery liest, useMutation schreibt

useQuery ist fuer das Lesen von Server-State zustaendig und haelt die Ergebnisse in einem zentralen Cache vor, den mehrere Komponenten gleichzeitig abonnieren koennen. useMutation uebernimmt den komplementaeren Fall, das Veraendern dieses Zustands ueber POST-, PUT-, PATCH- oder DELETE-Requests, und ist der vorgesehene Ort, um Nebenwirkungen wie Cache-Updates nach einer erfolgreichen Aenderung auszuloesen.

Anders als bei einem manuellen Ansatz mit fetch und lokalem useState kapselt useMutation Status-Informationen wie isPending, isError und isSuccess, eingebaute Retry-Logik sowie eine feste Reihenfolge an Callback-Hooks, die sich nahtlos mit demselben Query-Cache verzahnen, den auch useQuery verwendet.

2. Die Grundlagen von useMutation

useMutation({ mutationFn, onMutate, onError, onSuccess, onSettled }) nimmt die eigentliche Request-Funktion sowie eine Reihe optionaler Callbacks entgegen. mutate() loest die Mutation feuer-und-vergiss aus, mutateAsync() gibt zusaetzlich ein Promise zurueck, falls auf das Ergebnis direkt im aufrufenden Code gewartet werden soll. onMutate laeuft synchron vor dem eigentlichen Request und ist der richtige Ort fuer optimistische Updates.

onSettled laeuft immer, unabhaengig davon ob die Mutation erfolgreich war oder fehlgeschlagen ist, und eignet sich deshalb als zentraler Ort fuer abschliessende invalidateQueries-Aufrufe. So bleibt der Cache in beiden Faellen wieder synchron zum tatsaechlichen Server-Zustand, waehrend onSuccess und onError jeweils nur fuer den passenden Ausgang zustaendig sind.


import { useMutation, useQueryClient } from '@tanstack/react-query';

function useUpdateTodo() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (todo) => api.updateTodo(todo.id, todo),
    onSuccess: () => {
      console.log('Todo erfolgreich aktualisiert');
    },
    onError: (error) => {
      console.error('Mutation fehlgeschlagen', error);
    },
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['todos'] });
    },
  });
}

3. Optimistische Updates umsetzen

Bei einem optimistischen Update wird die Benutzeroberflaeche sofort aktualisiert, noch bevor die Antwort des Servers eintrifft. In onMutate wird dafuer per queryClient.setQueryData direkt in den Cache geschrieben, mit exakt dem Wert, den die UI zeigen soll, sobald die Mutation erfolgreich ist.

Bevor der Cache optimistisch geschrieben wird, muss queryClient.cancelQueries fuer denselben Query-Key aufgerufen werden. Ohne diesen Schritt kann ein noch laufendes Hintergrund-Refetch das optimistische Update ueberschreiben, waehrend die Mutation noch unterwegs ist, was zu einem kurzzeitigen, verwirrenden Zuruckspringen der Anzeige fuehrt.


function useUpdateTodo() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (todo) => api.updateTodo(todo.id, todo),
    onMutate: async (newTodo) => {
      await queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] });
      const previous = queryClient.getQueryData(['todos', newTodo.id]);

      queryClient.setQueryData(['todos', newTodo.id], newTodo);

      return { previous };
    },
  });
}

4. Rollback bei fehlgeschlagenen Mutationen

onMutate kann einen context-Wert zurueckgeben, ueblicherweise einen Snapshot des alten Cache-Werts vor dem optimistischen Schreiben. Dieser context wird automatisch als drittes Argument an onError weitergereicht und ermoeglicht dort, den Cache mit setQueryData exakt auf den Zustand vor der Mutation zurueckzusetzen.

Ohne diesen Snapshot-Mechanismus bliebe bei einem fehlgeschlagenen Request ein optimistischer, nie vom Server bestaetigter Zustand dauerhaft in der Oberflaeche sichtbar. Die Kombination aus Snapshot in onMutate und Rollback in onError ist deshalb keine Kuer, sondern eine notwendige Voraussetzung, sobald ueberhaupt optimistische Updates zum Einsatz kommen.


function useUpdateTodo() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (todo) => api.updateTodo(todo.id, todo),
    onMutate: async (newTodo) => {
      await queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] });
      const previous = queryClient.getQueryData(['todos', newTodo.id]);
      queryClient.setQueryData(['todos', newTodo.id], newTodo);
      return { previous };
    },
    onError: (error, newTodo, context) => {
      // Rollback auf den Zustand vor der optimistischen Aenderung
      queryClient.setQueryData(['todos', newTodo.id], context.previous);
    },
  });
}

5. Gezielte Cache-Invalidierung

invalidateQueries({ queryKey }) markiert alle passenden Queries als veraltet und laedt aktiv abonnierte davon automatisch neu. Entscheidend fuer eine praezise Invalidierung ist eine granulare Query-Key-Struktur, etwa ['todos', 'list', { filters }] statt eines pauschalen ['todos'], das alles unter diesem Praefix trifft.

Eine zu breite Invalidierung loest unnoetig viele gleichzeitige Refetches aus und belastet Netzwerk sowie Server ohne echten Mehrwert. Eine zu enge Invalidierung uebersieht dagegen abhaengige Queries, etwa eine Detailansicht eines Todos, das gerade in der Liste geloescht wurde und dessen Detail-Query danach ebenfalls veraltet ist, aber unberuehrt bleibt.


// Trifft nur Listen-Queries mit passendem Praefix,
// nicht die einzelnen Detail-Queries pro Todo-ID
queryClient.invalidateQueries({ queryKey: ['todos', 'list'] });

// Trifft alle Queries, deren Key mit 'todos' beginnt,
// inklusive Listen- und Detail-Queries
queryClient.invalidateQueries({ queryKey: ['todos'] });

// Praedikat-basierte Invalidierung fuer komplexere Faelle
queryClient.invalidateQueries({
  predicate: (query) =>
    query.queryKey[0] === 'todos' && query.queryKey[1] === 'list',
});

6. Der Unterschied zu manuellem Refetch

Ein handgestricktes Muster, bei dem nach einem fetch-Aufruf in einem Event-Handler ein erneuter fetch in einem useEffect oder ein kompletter Seiten-Reload folgt, synchronisiert immer nur die eine Komponente, die diesen Code enthaelt. Andere Komponenten, die denselben Server-Zustand anzeigen, etwa ein Zaehler in der Navigationsleiste, bleiben veraltet, bis sie selbststaendig neu laden.

useMutation in Kombination mit invalidateQueries synchronisiert dagegen automatisch alle Komponenten, die denselben Query-Key abonniert haben, unabhaengig davon, an welcher Stelle im Komponentenbaum die Mutation ausgeloest wurde. Das ist der eigentliche strukturelle Mehrwert gegenueber handgerollter Fetch-Logik, nicht nur weniger Code, sondern konsistenter Zustand ueber die gesamte Anwendung hinweg.

7. Reihenfolge bei schnellen aufeinanderfolgenden Mutationen

Schnell aufeinanderfolgende Mutationen, etwa ein versehentlicher Doppelklick auf einen Like-Button, koennen je nach Netzwerkbedingungen in vertauschter Reihenfolge beim Server ankommen. mutationKey zusammen mit den Optionen networkMode und retry hilft dabei, Wiederholverhalten und Netzwerkverhalten fuer eine Gruppe zusammengehoeriger Mutationen konsistent zu steuern.

Fuer streng sequentielle Mutationen, bei denen die Reihenfolge fachlich zwingend eingehalten werden muss, empfiehlt sich eine eigene Warteschlange, etwa eine async Funktion, die Mutationen nacheinander per await mutateAsync() abarbeitet. Fuer den einfacheren, aber sehr haeufigen Fall reicht es meist, den ausloesenden Button waehrend isPending zu deaktivieren, um Doppel-Submits von vornherein zu verhindern.


function LikeButton({ postId }) {
  const { mutate, isPending } = useMutation({
    mutationFn: () => api.likePost(postId),
  });

  return (
    <button disabled={isPending} onClick={() => mutate()}>
      {isPending ? 'Wird gesendet...' : 'Gefaellt mir'}
    </button>
  );
}

8. Optimistische Updates bei Listen

Bei Listen wie einer Todo-Liste oder Kommentaren betrifft das optimistische Update nicht einen einzelnen Wert, sondern ein ganzes Array im Cache. Ein neues Element wird per setQueryData((old) => [...old, tempItem]) eingefuegt, ein geloeschtes Element entsprechend per filter aus dem bestehenden Array entfernt, jeweils innerhalb von onMutate.

Fuer ein neu eingefuegtes Element empfiehlt sich eine clientseitig generierte temporaere ID, etwa per crypto.randomUUID(), solange die echte, vom Server vergebene ID noch nicht bekannt ist. Nach einer erfolgreichen Antwort wird das temporaere Element in onSuccess entweder durch das echte Server-Objekt ersetzt oder der betroffene Query-Key komplett per invalidateQueries neu geladen.


function useAddComment(postId) {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (text) => api.addComment(postId, text),
    onMutate: async (text) => {
      await queryClient.cancelQueries({ queryKey: ['comments', postId] });
      const previous = queryClient.getQueryData(['comments', postId]);
      const tempItem = { id: crypto.randomUUID(), text, pending: true };

      queryClient.setQueryData(['comments', postId], (old = []) => [...old, tempItem]);
      return { previous };
    },
    onError: (err, text, context) => {
      queryClient.setQueryData(['comments', postId], context.previous);
    },
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['comments', postId] });
    },
  });
}

9. Haeufige Fehler bei Mutations

Der haeufigste Fehler ist ein optimistisches Update in onMutate ohne passenden Rollback in onError. Schlaegt der Request fehl, bleibt der optimistisch geschriebene, aber nie vom Server bestaetigte Zustand dauerhaft in der Oberflaeche sichtbar, was besonders bei geloeschten oder veraenderten Eintraegen zu verwirrenden, inkonsistenten Ansichten fuehrt.

Ein zweiter verbreiteter Fehler ist ein falscher oder zu spezifischer Query-Key bei invalidateQueries, sodass verwandte Queries wie eine Detailseite, ein Zaehler oder eine gefilterte Liste nicht mit aktualisiert werden. Dazu kommt haeufig fehlendes cancelQueries vor dem optimistischen Schreiben, wodurch ein zeitgleiches Hintergrund-Refetch das optimistische Update kommentarlos wieder ueberschreibt.

Strategie UI-Reaktionszeit Fehlerbehandlung Wann einsetzen
Manuelles refetch nach Mutation Verzoegert bis zur Serverantwort plus erneutem Request Manuell im Handler, oft inkonsistent Nur bei sehr einfachen Prototypen
useMutation ohne optimistic update Verzoegert bis zur Serverantwort Ueber onError zentral geregelt Wenn UI-Latenz akzeptabel ist
useMutation mit optimistic update und Rollback Sofort, noch vor der Serverantwort Automatischer Rollback per Snapshot Bei haeufigen Interaktionen wie Likes, Toggles
useMutation mit gezielter invalidateQueries Sofort sichtbar nach erfolgreicher Mutation Konsistent ueber alle abonnierten Komponenten Bei Aenderungen mit mehreren betroffenen Ansichten

Mironsoft

React-Architektur, Performance und Magento-Frontend-Integration

React-Frontends, die schnell bleiben statt mit jedem Feature langsamer zu werden?

Wir prüfen bestehende React-Anwendungen auf unnötige Re-Renders, aufgeblähte Bundles und fragile State-Verwaltung und bauen daraus ein Frontend, das performant bleibt und sich sauber an Magento oder andere Backends anbindet.

Performance-Audit

Re-Renders, Bundle-Größe und Ladezeiten systematisch messen und beheben.

State-Architektur

Context, Zustand und Server State sauber trennen statt alles in einen Topf zu werfen.

Magento-Integration

GraphQL- oder REST-Anbindung an Magento robust und typsicher aufbauen.

10. Zusammenfassung

TanStack Query Mutations: Das Wichtigste auf einen Blick

onMutate

Laeuft synchron vor dem Request, Ort fuer optimistisches setQueryData und Snapshot des alten Werts.

onError

Erhaelt den Snapshot aus onMutate als context und fuehrt damit den Rollback auf den alten Zustand durch.

onSettled

Laeuft immer, unabhaengig vom Ausgang, richtiger Ort fuer abschliessende invalidateQueries-Aufrufe.

Query-Keys

Granulare Query-Key-Struktur ermoeglicht praezise Invalidierung statt pauschalem, teurem Refetch.

11. FAQ: TanStack Query Mutations: Das Wichtigste auf einen Blick

1Was ist der Unterschied zwischen useQuery und useMutation?
useQuery liest Server-State und haelt ihn im Cache vor, useMutation veraendert diesen Zustand ueber POST, PUT, PATCH oder DELETE Requests. useMutation ist der vorgesehene Ort, um Nebenwirkungen wie Cache-Updates nach einer erfolgreichen Aenderung auszuloesen.
2Wann laeuft onMutate im Vergleich zu onSuccess und onError?
onMutate laeuft synchron, bevor der eigentliche Request abgeschickt wird, und eignet sich deshalb fuer optimistische Updates. onSuccess laeuft nur bei erfolgreicher Antwort, onError nur bei einem fehlgeschlagenen Request, onSettled dagegen laeuft in beiden Faellen.
3Warum muss vor einem optimistischen Update cancelQueries aufgerufen werden?
Ohne cancelQueries kann ein noch laufendes Hintergrund-Refetch fuer denselben Query-Key das gerade geschriebene optimistische Update ueberschreiben, waehrend die Mutation noch unterwegs ist. Das fuehrt zu einem kurzzeitigen, verwirrenden Zuruckspringen der Anzeige.
4Wie funktioniert der Rollback bei einer fehlgeschlagenen optimistischen Mutation?
onMutate gibt einen context mit einem Snapshot des alten Cache-Werts zurueck. Dieser context wird automatisch als drittes Argument an onError weitergereicht, wo per setQueryData der Cache exakt auf den Zustand vor der Mutation zurueckgesetzt wird.
5Warum ist eine granulare Query-Key-Struktur wichtig?
Eine granulare Struktur wie ['todos', 'list', filters] statt eines pauschalen ['todos'] ermoeglicht praezise Invalidierung. Zu breite Invalidierung loest unnoetig viele Refetches aus, zu enge uebersieht abhaengige Queries wie eine Detailansicht.
6Was ist der Vorteil von useMutation gegenueber manuellem fetch plus useEffect?
useMutation mit invalidateQueries synchronisiert automatisch alle Komponenten, die denselben Query-Key abonniert haben, unabhaengig davon wo im Komponentenbaum die Mutation ausgeloest wurde. Manuelles fetch plus useEffect synchronisiert dagegen nur die eine Komponente, die diesen Code enthaelt.
7Wie verhindere ich Doppel-Submits bei schnellen Klicks?
Der einfachste Weg ist, den ausloesenden Button waehrend isPending zu deaktivieren. Fuer streng sequentielle Mutationen, bei denen die Reihenfolge fachlich zwingend eingehalten werden muss, empfiehlt sich eine eigene Warteschlange mit await mutateAsync().
8Wie behandle ich optimistische Updates bei einer Liste statt einem einzelnen Wert?
Ein neues Element wird per setQueryData mit einer Funktion eingefuegt, die das bestehende Array um ein neues Element erweitert, ein geloeschtes Element entsprechend per filter entfernt. Fuer neue Elemente empfiehlt sich eine temporaere clientseitige ID bis die echte Server-ID bekannt ist.
9Was ist der Unterschied zwischen mutate() und mutateAsync()?
mutate() loest die Mutation feuer-und-vergiss aus und gibt kein Promise zurueck, Fehler werden ueber onError behandelt. mutateAsync() gibt zusaetzlich ein Promise zurueck, das sich mit await direkt im aufrufenden Code auswerten laesst, etwa fuer sequentielle Ablaeufe.
10Was passiert, wenn ich onError bei einem optimistischen Update vergesse?
Ohne Rollback in onError bleibt bei einem fehlgeschlagenen Request der optimistisch geschriebene, aber nie vom Server bestaetigte Zustand dauerhaft in der Oberflaeche sichtbar. Das fuehrt zu inkonsistenten Ansichten, die erst durch einen manuellen Reload wieder korrekt werden.