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

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.
- Vorteile: Benutzer bemerken keine Netzwerklatenz und empfinden die App als extrem schnell.
- Nachteile: Es entsteht zusätzliche Komplexität beim Zurücksetzen (Rollback) der UI auf den vorherigen Zustand, falls die Serveranfrage fehlschlägt.
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
onMutatezurückgegebene Kontextobjekt anonErrorundonSettledü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
cancelQueries: Verhindert, dass optimistische Updates durch veraltete Daten aus einer verzögerten Anfrage überschrieben werden.previousPostSnapshot: Speichert den ursprünglichen Zustand, bevor der Cache geändert wird.- Kontext-Rückgabe: Der von
onMutatezurückgegebene Wert wird als drittes Argument anonErrorzur 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.
online(Standard): Anfragen anhalten, bis das Netzwerk wiederhergestellt ist.always: Immer versuchen abzurufen und den Cache zu aktualisieren, unabhängig vom Netzwerkstatus.offlineFirst: Priorisiert die Verwendung des Offline-Caches bei gleichzeitiger Durchführung der Hintergrundsynchronisierung.
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:
- Debouncing auf der UI-Seite anwenden.
- Eine Update-Warteschlange lokal verwalten, bevor der Cache verändert wird.
- Bei Array-Daten (z. B. Hinzufügen eines Todos) eine temporäre ID (
uuid) vergeben und diese nach der Serverantwort durch die echte ID ersetzen.
| 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
- Optimistische Updates verbessern die UX drastisch, da sie den Benutzern das Gefühl einer sofortigen Reaktion geben.
- Behalten Sie das Muster bei, einen Snapshot in
onMutatezu speichern und inonErrorzurückzusetzen. - Um eine Offline-First-Architektur zu realisieren, kombinieren Sie den
offlineFirstNetwork-Mode mit IndexedDB-basierten Persistern. - Planen Sie Randfälle wie aufeinanderfolgende Ereignisse oder temporäres ID-Management im Voraus ein, um die Datenintegrität zu gewährleisten.