Fortgeschrittene Datenabfrage mit TanStack Query – Optimistische Aktualisierungen, Paginierung und WebSocket-Integration
Ethan Miller
Product Engineer · Leapcell

Einleitung
In der modernen Webentwicklungslandschaft sind der Aufbau performanter, reaktionsschneller und datenreicher Benutzeroberflächen von größter Bedeutung. Benutzer erwarten nahtlose Interaktionen und aktuelle Informationen. Während traditionelle Mechanismen zur Datenabfrage für einfache Szenarien oft ausreichen, erfordern die Anforderungen komplexer Anwendungen ausgefeiltere Lösungen. Hier glänzen Bibliotheken wie TanStack Query (ehemals React Query). Es bietet leistungsstarke Primitive für die Verwaltung von Serverzuständen und vereinfacht die Datenabfrage, das Caching, die Synchronisierung und die Fehlerbehandlung erheblich. Über seine grundlegenden Fähigkeiten hinaus bietet TanStack Query eine Reihe fortgeschrittener Funktionen, die die Benutzererfahrung und die Entwicklerproduktivität wirklich verbessern können. Dieser Artikel befasst sich mit drei solchen Schlüsselmerkmalen: optimistische Aktualisierungen für sofortiges Feedback, effiziente Paginierungsstrategien für die Handhabung großer Datensätze und nahtlose Integration mit WebSockets für die Echtzeit-Datensynchronisierung. Das Verständnis und die Nutzung dieser fortgeschrittenen Muster können Ihre Anwendung von bloß funktionell zu wunderbar reaktionsschnell und dynamisch verwandeln.
Kernkonzepte
Bevor wir uns mit den fortgeschrittenen Funktionen befassen, definieren wir kurz einige Kernkonzepte innerhalb von TanStack Query, auf die in dieser Diskussion verwiesen wird.
- Query: Stellt eine Anfrage zur Abfrage von Daten von Ihrem Backend dar. Queries werden durch einen eindeutigen
queryKey
identifiziert und automatisch von TanStack Query gecacht und erneut abgerufen. - Mutation: Stellt eine Operation dar, die Daten auf dem Backend modifiziert (z. B. Erstellen, Aktualisieren, Löschen). Mutationen haben oft Nebeneffekte und können die Query-Invalidierung auslösen.
- Query Client: Die zentrale Instanz, die alle Ihre Queries und Mutationen verwaltet. Sie hält den Cache und stellt Methoden für die Interaktion damit bereit.
- Query Cache: Wo TanStack Query die Ergebnisse Ihrer Queries zusammen mit ihren Metadaten speichert. Dieser persistente Cache ermöglicht die sofortige Darstellung zuvor abgerufener Daten.
- Invalidierung: Der Prozess, eine gecachte Query als „veraltet“ zu markieren, was TanStack Query veranlasst, sie beim nächsten Zugriff im Hintergrund erneut abzurufen. Dies gewährleistet die Aktualität der Daten.
Diese grundlegenden Konzepte ermöglichen es TanStack Query, den Serverzustand Ihrer Anwendung intelligent zu verwalten und eine robuste Plattform für die Implementierung fortgeschrittenerer Verhaltensweisen zu bieten.
Optimistische Aktualisierungen
Optimistische Aktualisierungen sind eine leistungsstarke Technik zur Verbesserung der wahrgenommenen Leistung und Reaktionsfähigkeit Ihrer Anwendung. Anstatt auf eine Serverantwort zu warten, bevor die Benutzeroberfläche aktualisiert wird, wendet eine optimistische Aktualisierung die erwarteten Änderungen sofort auf die Benutzeroberfläche an. Wenn die Serveroperation erfolgreich ist, bleibt die Benutzeroberfläche aktualisiert. Wenn sie fehlschlägt, wird die Benutzeroberfläche auf ihren vorherigen Zustand zurückgesetzt. Dies bietet dem Benutzer sofortiges Feedback und lässt die Anwendung viel schneller erscheinen.
Wie es funktioniert
Die Kernidee hinter optimistischen Aktualisierungen ist es, auf der Clientseite vom „Erfolg auszugehen“. Wenn eine Mutation initiiert wird, tun wir Folgendes:
- Brich alle ausgehenden Queries ab, die die optimistische Aktualisierung beeinträchtigen könnten.
- Schnappschuss der aktuellen Query-Daten, die von der Mutation betroffen sind. Dies ermöglicht ein reibungsloses Rollback, falls die Mutation fehlschlägt.
- Aktualisiere die Benutzeroberfläche optimistisch, indem die gecachten Query-Daten direkt geändert werden, um das erwartete Ergebnis der Mutation widerzuspiegeln.
- Führe die eigentliche Mutation auf dem Server aus.
- Bei Erfolg: Ungültige die betroffenen Queries, um die frischen Daten vom Server erneut abzurufen und die Konsistenz zu gewährleisten.
- Bei Fehler: Rolle die Benutzeroberfläche auf den in Schritt 2 aufgenommenen Schnappschuss zurück.
Implementierungsbeispiel
Betrachten wir ein Szenario, in dem ein Benutzer den „abgeschlossenen“-Status eines Todo-Elements umschaltet.
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { updateTodoApi } from './api'; // Angenommen, dies ist ein API-Aufruf function TodoItem({ todo }) { const queryClient = useQueryClient(); const { mutate } = useMutation({ mutationFn: updateTodoApi, onMutate: async (newTodo) => { // Schritt 1: Abbrechen aller ausgehenden erneuten Abrufe für die Todos-Query await queryClient.cancelQueries({ queryKey: ['todos'] }); // Schritt 2: Schnappschuss des vorherigen Werts const previousTodos = queryClient.getQueryData(['todos']); // Schritt 3: Optimistische Aktualisierung des Caches queryClient.setQueryData(['todos'], (old) => old ? old.map((t) => (t.id === newTodo.id ? newTodo : t)) : [] ); // Rückgabe eines Kontextobjekts mit dem Schnappschuss return { previousTodos }; }, onError: (err, newTodo, context) => { // Schritt 6: Rollback bei Fehler queryClient.setQueryData(['todos'], context.previousTodos); console.error('Fehler beim Aktualisieren des Todos:', err); }, onSettled: () => { // Schritt 5: Ungültige Queries abrufen, um frische Daten nach der Mutation zu gewährleisten queryClient.invalidateQueries({ queryKey: ['todos'] }); }, }); const toggleComplete = () => { mutate({ ...todo, completed: !todo.completed }); }; return ( <div> <input type="checkbox" checked={todo.completed} onChange={toggleComplete} /> <span>{todo.title}</span> </div> ); }
In diesem Beispiel wird beim Klicken auf das Kontrollkästchen die Benutzeroberfläche sofort aktualisiert, als ob die Anfrage erfolgreich gewesen wäre. Wenn der Aufruf updateTodoApi
fehlschlägt, wird die Benutzeroberfläche ordnungsgemäß auf ihren vorherigen Zustand zurückgesetzt. Dies bietet ein deutlich reibungsloseres Benutzererlebnis im Vergleich zum Warten auf einen Server-Roundtrip.
Anwendungsszenarien
Optimistische Aktualisierungen sind ideal für Aktionen, bei denen sofortiges visuelles Feedback entscheidend ist und die Wahrscheinlichkeit eines Fehlers relativ gering ist. Häufige Szenarien sind:
- Umschalten von Kontrollkästchen (z. B. Abschluss von Todos, Markieren als gelesen).
- Likes/Dislikes von Beiträgen.
- Hinzufügen/Entfernen von Artikeln aus einem Warenkorb (mit reibungsloser Fehlerbehandlung bei Lagerproblemen).
- Einfache Formularübermittlungen, bei denen eine sofortige Bestätigung hilfreich ist.
Paginierungs-Queries
Die effektive Handhabung großer Datensätze ist eine häufige Herausforderung in der Webentwicklung. Tausende von Datensätzen auf einmal anzuzeigen, ist ineffizient und beeinträchtigt die Leistung. Paginierung ist eine weit verbreitete Lösung, die es Benutzern ermöglicht, durch Daten in überschaubaren Blöcken zu blättern. TanStack Query bietet robuste Funktionen zur Implementierung verschiedener Paginierungsstrategien, die eine effiziente Datenabfrage und ein reibungsloses Benutzererlebnis gewährleisten.
Arten der Paginierung
Es gibt hauptsächlich zwei Arten der Paginierung:
- Offset-basierte Paginierung: Dies ist die häufigste Form, bei der Sie Daten basierend auf einer
page
-Zahl und einemlimit
(oderper_page
) abfragen. Der Server gibt einen bestimmten „Offset“ von Einträgen aus der Gesamtliste zurück. - Cursor-basierte Paginierung (unendliches Scrollen): Diese Methode fragt Daten basierend auf einem „Cursor“ (normalerweise einer ID oder einem Zeitstempel) aus der zuvor abgerufenen Menge ab. Sie wird oft für unendliche Scroll-Erlebnisse verwendet, bei denen neue Daten hinzugefügt werden, während der Benutzer nach unten scrollt.
Offset-basierte Paginierung mit useQuery
Für die Standard-Seitenavigation ist useQuery
bestens geeignet.
import { useQuery } from '@tanstack/react-query'; import { fetchPostsApi } from './api'; // Angenommen, die API ruft Beiträge nach Seite ab function PostsList() { const [page, setPage] = useState(0); const { data, isPreviousData, isLoading, isError, error } = useQuery({ queryKey: ['posts', page], // queryKey ändert sich mit der Seitenzahl queryFn: () => fetchPostsApi(page), keepPreviousData: true, // Behalte zuvor abgerufene Daten bei, während neue Daten geladen werden }); if (isLoading) return <div>Lade Beiträge...</div>; if (isError) return <div>Fehler: {error.message}</div>; return ( <div> {data.posts.map((post) => ( <div key={post.id}>{post.title}</div> ))} <button onClick={() => setPage((old) => Math.max(old - 1, 0))} disabled={page === 0} > Zurück </button> <button onClick={() => { if (!isPreviousData && data.hasMore) { // Stelle sicher, dass hasMore von deiner API zurückgegeben wird setPage((old) => old + 1); } }} disabled={isPreviousData || !data.hasMore} > Weiter </button> <span>Aktuelle Seite: {page + 1}</span> </div> ); }
Durch das Hinzufügen von page
zum queryKey
behandelt TanStack Query die Daten jeder Seite als separaten Eintrag im Cache. Die Option keepPreviousData: true
ist hier entscheidend; sie ermöglicht, dass die zuvor abgerufenen Daten sichtbar bleiben, während die Daten der neuen Seite geladen werden, wodurch ein störender leerer Zustand vermieden und die Benutzererfahrung verbessert wird.
Cursor-basierte Paginierung mit useInfiniteQuery
Für unendliches Scrollen oder „Mehr laden“-Muster ist useInfiniteQuery
die beste Lösung. Es wurde speziell entwickelt, um Listen abzurufen und zu verwalten, die sich im Laufe der Zeit erweitern.
import { useInfiniteQuery } from '@tanstack/react-query'; import { fetchCommentsApi } from './api'; // Angenommen, die API ruft Kommentare mit einem 'cursor' ab function CommentsFeed() { const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isError, error, } = useInfiniteQuery({ queryKey: ['comments'], queryFn: ({ pageParam }) => fetchCommentsApi(pageParam), // pageParam ist der Cursor initialPageParam: undefined, getNextPageParam: (lastPage) => lastPage.nextCursor, // API sollte einen nextCursor zurückgeben }); if (isLoading) return <div>Lade Kommentare...</div>; if (isError) return <div>Fehler: {error.message}</div>; return ( <div> {data.pages.map((page, i) => ( <React.Fragment key={i}> {page.comments.map((comment) => ( <div key={comment.id}>{comment.text}</div> ))} </React.Fragment> ))} <button onClick={() => fetchNextPage()} disabled={!hasNextPage || isFetchingNextPage} > {isFetchingNextPage ? 'Mehr laden...' : hasNextPage ? 'Mehr laden' : 'Nichts mehr zu laden'} </button> </div> ); }
useInfiniteQuery
speichert die Daten in einem abgeflachten Array, gruppiert nach Seiten. getNextPageParam
ist eine kritische Funktion, die TanStack Query mitteilt, wie der pageParam
für die nächste Abfrage abgerufen wird, typischerweise ein von der Serverantwort bereitgestellter Cursor (lastPage.nextCursor
). Dieses Muster ist hocheffizient, da es nur bei Bedarf neue Daten abruft, was die anfänglichen Ladezeiten und die Serverlast reduziert.
Anwendungsszenarien
- Offset-basierte Paginierung: Admin-Dashboards, Suchergebnisse, E-Commerce-Produktlisten mit festen Seitenzahlen.
- Cursor-basierte Paginierung: Social-Media-Feeds, Aktivitätsprotokolle, Chatverläufe, jedes „unendliche Scroll“-Erlebnis.
WebSocket-Integration
Während TanStack Query hervorragend für die Verwaltung von Serverzuständen von RESTful APIs geeignet ist, erfordern moderne Anwendungen oft Echtzeit-Updates über WebSockets. Die Integration von WebSockets mit TanStack Query ermöglicht es Ihnen, Echtzeitänderungen direkt in den Query Cache zu pushen und sicherzustellen, dass Ihre Benutzeroberfläche stets den neuesten Zustand widerspiegelt, ohne ständiges Polling oder manuelle erneute Abrufe.
Die Herausforderung
Ohne richtige Integration werden Echtzeit-Updates von einem WebSocket möglicherweise nicht automatisch in Ihren von TanStack Query verwalteten Daten angezeigt. Sie müssten normalerweise den Komponentenstatus manuell aktualisieren oder erneute Abrufe auslösen, was zu Inkonsistenzen und Boilerplate-Code führt.
Lösung mit queryClient.setQueryData
und queryClient.invalidateQueries
Der Schlüssel zur Integration von WebSockets liegt in der Nutzung von queryClient.setQueryData
, um den Cache direkt zu aktualisieren, und queryClient.invalidateQueries
, um bei Bedarf erneute Abrufe auszulösen.
import React, { useEffect } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { fetchStockPricesApi } from './api'; // API zum Abrufen anfänglicher Aktienkurse // Angenommen werden ein WebSocket-Verbindungsdienstprogramm const connectWebSocket = (onMessage) => { const ws = new WebSocket('ws://localhost:8080/stock-prices'); ws.onmessage = (event) => onMessage(JSON.parse(event.data)); return ws; }; function StockPricesDisplay() { const queryClient = useQueryClient(); // Anfängliche Aktienkurse abrufen const { data: stockPrices, isLoading, isError, error } = useQuery({ queryKey: ['stockPrices'], queryFn: fetchStockPricesApi, }); useEffect(() => { const ws = connectWebSocket((newPrice) => { // Einen bestimmten Aktienkurs im Cache aktualisieren queryClient.setQueryData(['stockPrices'], (oldPrices) => { if (!oldPrices) return [newPrice]; // Umgang mit dem anfänglichen Zustand, wenn der Cache leer ist return oldPrices.map((price) => price.symbol === newPrice.symbol ? newPrice : price ); }); // Optional: Queries invalidieren, wenn ein vollständiger erneuter Abruf für andere Daten gewünscht wird // Zum Beispiel, wenn ein "Portfolio-Gesamtbetrag" von einzelnen Aktienkursen abhängt // queryClient.invalidateQueries({ queryKey: ['portfolioTotal'] }); }); return () => ws.close(); // WebSocket-Verbindung bei Unmount bereinigen }, [queryClient]); if (isLoading) return <div>Lade Aktienkurse...</div>; if (isError) return <div>Fehler: {error.message}</div>; return ( <div> <h2>Echtzeit-Aktienkurse</h2> {stockPrices.map((stock) => ( <div key={stock.symbol}> {stock.symbol}: ${stock.price.toFixed(2)} <span style={{ color: stock.change > 0 ? 'green' : 'red' }}> ({stock.change > 0 ? '+' : ''} {stock.change.toFixed(2)}%) </span> </div> ))} </div> ); }
In diesem Beispiel:
- Wir stellen beim Mounten der Komponente eine WebSocket-Verbindung her.
- Wenn eine neue Nachricht (z. B. ein aktualisierter Aktienkurs) vom WebSocket empfangen wird, parsen wir sie.
- Anschließend verwenden wir
queryClient.setQueryData(['stockPrices'], ...)
, um diestockPrices
-Query im Cache direkt zu aktualisieren. Diese Änderung löst sofort eine erneute Darstellung aller Komponenten aus, die diese Query verwenden, und spiegelt die Echtzeitaktualisierung wider. - Optional können Sie, wenn ein eingehendes WebSocket-Ereignis abgeleitete Daten beeinflusst (z. B. eine Summe, die aus einer Liste von Elementen berechnet wird),
queryClient.invalidateQueries
verwenden, um einen erneuten Abruf dieser abhängigen Queries auszulösen.
Dieser Ansatz bietet eine leistungsstarke Möglichkeit, die Echtzeitfunktionen von WebSockets mit der robusten Zustandsverwaltung von TanStack Query zu verbinden und so eine überlegene Benutzererfahrung mit minimaler manueller Zustandsverwaltung zu bieten.
Anwendungsszenarien
- Echtzeit-Dashboards: Aktienticker, Kryptowährungskurse, Live-Analysen.
- Chat-Anwendungen: Sofortige Nachrichtenübermittlung.
- Benachrichtigungen: Push von Echtzeit-Benachrichtigungen an Benutzer.
- Kollaborative Bearbeitung: Synchronisierung von Änderungen über mehrere Benutzer.
Fazit
TanStack Query geht weit über die grundlegende Datenabfrage hinaus und bietet ein Ökosystem leistungsstarker Werkzeuge zur Verwaltung komplexer Serverzustände. Durch die Beherrschung fortgeschrittener Funktionen wie optimistische Aktualisierungen, ausgefeilte Paginierung und WebSocket-Integration können Entwickler Anwendungen erstellen, die nicht nur effizient und robust, sondern auch außergewöhnlich reaktionsschnell und dynamisch sind. Diese Techniken tragen erheblich zur wahrgenommenen Leistung und zu einer angenehmen Benutzererfahrung bei und stellen sicher, dass Ihre Anwendungen in der heutigen anspruchsvollen digitalen Landschaft hervorstechen. TanStack Query ermöglicht es Ihnen, hochgradig ansprechende Echtzeit-Benutzeroberflächen mit Zuversicht und Leichtigkeit zu erstellen.