effidevFlutter · Cloudflare-Edge · Cloud-Kostenoptimierung
Deutsch

TanStack Query v5 in der Praxis: Optimistische Updates und Offline-Caching-Muster

Architekturdiagramm zu optimistischen Updates und Offline-Caching in TanStack Query v5

In modernen Webanwendungen ist die Benutzererfahrung (UX) der zentrale Faktor für den Erfolg einer App. Stellen Sie sich vor, die Benutzeroberfläche würde sofort reagieren, anstatt einen Ladespinner anzuzeigen, wenn ein Benutzer auf eine Schaltfläche klickt.

In diesem Artikel befassen wir uns ausführlich mit der Implementierung von optimistischen Updates (Optimistic Updates), um mit TanStack Query v5 (ehemals React Query) eine Latenz-freie Erfahrung zu bieten, und untersuchen Offline-Caching-Muster für den Fall eines Verbindungsabbruchs.

1. Was sind optimistische Updates?

Optimistische Updates sind eine Technik, bei der die Benutzeroberfläche sofort aktualisiert wird, indem optimistisch davon ausgegangen wird, dass die Anfrage erfolgreich sein wird, ohne auf die Antwort des Servers zu warten.

In TanStack Query v5 lässt sich dieser Rollback-Mechanismus elegant über die Lifecycle-Callbacks des useMutation-Hooks (onMutate, onError, onSettled) handhaben.

[!NOTE] In v5 wird das von onMutate zurückgegebene Kontextobjekt an onError und onSettled übergeben und für Rollbacks verwendet.

2. Implementierung von optimistischen Updates mit TanStack Query v5

Sehen wir uns als Beispiel eine einfache “Gefällt mir”-Umschaltfunktion an.

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

// Post-Typ Definition
interface Post {
  id: string;
  title: string;
  likes: number;
}

export function useToggleLike() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (postId: string) => fetch(`/api/posts/${postId}/like`, { method: 'POST' }),
    
    // onMutate: Wird aufgerufen, bevor die Mutationsfunktion ausgeführt wird
    onMutate: async (newPostId) => {
      // 1. Laufende Refetches abbrechen, um ein Überschreiben unseres optimistischen Updates zu verhindern
      await queryClient.cancelQueries({ queryKey: ['posts', newPostId] });

      // 2. Einen Snapshot des vorherigen Zustands für die Wiederherstellung im Fehlerfall speichern
      const previousPost = queryClient.getQueryData<Post>(['posts', newPostId]);

      // 3. Den Cache optimistisch aktualisieren
      if (previousPost) {
        queryClient.setQueryData<Post>(['posts', newPostId], {
          ...previousPost,
          likes: previousPost.likes + 1,
        });
      }

      // 4. Einen Kontext mit den vorherigen Daten zurückgeben
      return { previousPost };
    },
    
    // onError: Mit dem von onMutate zurückgegebenen Kontext auf den vorherigen Zustand zurücksetzen
    onError: (err, newPostId, context) => {
      if (context?.previousPost) {
        queryClient.setQueryData(['posts', newPostId], context.previousPost);
      }
    },
    
    // onSettled: Caches entwerten, um unabhängig von Erfolg oder Misserfolg aktuelle Daten zu gewährleisten
    onSettled: (newPostId) => {
      queryClient.invalidateQueries({ queryKey: ['posts', newPostId] });
    },
  });
}

Analyse der Kernpunkte

  1. cancelQueries: Verhindert, dass optimistische Updates durch veraltete Daten aus einer verzögerten Anfrage überschrieben werden.
  2. previousPost Snapshot: Speichert den ursprünglichen Zustand, bevor der Cache geändert wird.
  3. Kontext-Rückgabe: Der von onMutate zurückgegebene Wert wird als drittes Argument an onError zur Verwendung bei Rollbacks übergeben.

3. Offline First und Caching-Muster

Um zu verhindern, dass Benutzer bei fehlender oder instabiler Netzwerkverbindung einen leeren Bildschirm sehen, ist eine geeignete Caching-Strategie erforderlich. TanStack Query v5 bietet standardmäßig robuste Offline-Mechanismen.

3.1 Network Mode konfigurieren

Der Standard-networkMode von TanStack Query ist online. Das bedeutet, dass Abfragen und Mutationen bei fehlender Netzwerkverbindung angehalten werden.

Wenn Sie eine Offline-First-App entwickeln, empfiehlt es sich, die Einstellung offlineFirst zu verwenden, um zwischengespeicherte Daten anzuzeigen und bei erneuter Verbindung eine Synchronisierung zu versuchen.

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      networkMode: 'offlineFirst',
      staleTime: 1000 * 60 * 5, // 5 Minuten
    },
  },
});

3.2 Dauerhaftes Caching mit Persisters

Um Daten auch nach einer Aktualisierung oder nach Schließen und erneutem Öffnen des Browsers beizubehalten, benötigen Sie ein Persister-Plugin. In v5 werden üblicherweise asynchrone Persister verwendet, die auf @tanstack/query-sync-storage-persister oder idb-keyval (IndexedDB) basieren.

Beispiel für Cache-Persistenz mit IndexedDB:

import { QueryClient } from '@tanstack/react-query';
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
import { get, set, del } from 'idb-keyval';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      cacheTime: 1000 * 60 * 60 * 24, // Cache für 24 Stunden behalten
    },
  },
});

const indexedDBPersister = createAsyncStoragePersister({
  storage: {
    getItem: async (key) => await get(key),
    setItem: async (key, value) => await set(key, value),
    removeItem: async (key) => await del(key),
  },
});

// App auf höchster Ebene mit dem Provider umschließen
function App() {
  return (
    <PersistQueryClientProvider
      client={queryClient}
      persistOptions={{ persister: indexedDBPersister }}
    >
      <YourApp />
    </PersistQueryClientProvider>
  );
}

4. Praktische Grenzfälle (Edge Cases) und Architekturentscheidungen

Bei der Anwendung von optimistischen Updates und Offline-Caching in der Produktion sind einige Grenzfälle zu beachten.

4.1 Schnelle aufeinanderfolgende optimistische Updates

Stellen Sie sich ein Szenario vor, in dem ein Benutzer schnell mehrmals auf “Gefällt mir” klickt. Wenn onMutate jedes Mal ausgelöst wird, könnte der Snapshot einen Zwischenzustand erfassen.

Lösungen:

Problem Ursache Lösungsmuster
Rollback schlägt fehl Fehlender Cache-Snapshot vor onMutate Immer queryClient.getQueryData als Kontext zurückgeben
Daten überschrieben Laufende GET-Anfrage trifft nach POST ein queryClient.cancelQueries ganz oben in onMutate platzieren
Flackern bei Listen Abweichung zwischen temporärer und Server-ID Das temporäre Objekt mit der Serverantwort via map ersetsetzen

Zusammenfassung