</>
{ }
React · TanStack Query v5 · Server State · Caching
TanStack Query v5:
Server State meistern

Manuell verwalteter Server State in React führt immer zu denselben Problemen: doppelte Requests, veraltete Daten, inkonsistente Ladezustände und Boilerplate, der sich über hunderte Komponenten verteilt. TanStack Query v5 löst das durch ein deklaratives Caching- und Synchronisationssystem, das Serverantworten zu einem reaktiven, automatisch synchronisierten Teil des UI-State macht.

16 Min. Lesezeit useQuery · useMutation · QueryClient · prefetch · optimistic updates TanStack Query 5.x · React 18/19 · TypeScript

1. Was Server State von Client State unterscheidet

Der entscheidende konzeptionelle Unterschied liegt im Eigentümer der Daten. Client State – ob ein Modal offen ist, welcher Tab aktiv ist, was im Formularfeld steht – gehört der Applikation und ändert sich nur durch Benutzerinteraktionen. Server State hingegen gehört dem Backend: eine Produktliste, ein Benutzerprofil, der Inhalt eines CMS-Eintrags. Der Server kann sich jederzeit ändern, ohne dass die Applikation es weiß. Jeder Ansatz, Server State mit useState und useEffect zu verwalten, kämpft gegen diesen Unterschied an – und verliert meistens.

Die konkreten Probleme manueller Server-State-Verwaltung kennt jeder React-Entwickler: Das Laden derselben Daten in zwei verschiedenen Komponenten erzeugt zwei parallele Requests. Nach einer Mutation sind andere Teile der UI veraltet, bis der Nutzer manuell neu lädt. Error States werden inkonsistent behandelt. Loading-Indikatoren erscheinen und verschwinden unkoordiniert. TanStack Query v5 löst all das durch einen zentralen Cache mit Subscription-Mechanismus: Mehrere Komponenten, die dieselben Daten benötigen, teilen sich automatisch einen Request und denselben Cache-Eintrag.

Version 5 brachte dabei wichtige API-Verbesserungen gegenüber v4: Der status-Union hat jetzt "pending" statt "loading", was semantisch korrekter ist. Das isLoading-Flag wurde durch isPending ergänzt. Die Objekt-Signatur für useQuery ist jetzt die einzige Form – kein Array-Parameter mehr. Und cacheTime heißt jetzt korrekt gcTime (Garbage Collection Time), was die tatsächliche Bedeutung besser beschreibt.

2. QueryClient und QueryClientProvider einrichten

Die QueryClient-Instanz ist das Herzstück von TanStack Query v5. Sie hält den gesamten Cache, verwaltet Background-Refetches und konfiguriert globale Standardwerte. Die Instanz wird einmal pro Applikation erstellt und über den QueryClientProvider im Komponentenbaum bereitgestellt. Für React Server Components in Next.js gibt es das HydrationBoundary-Muster: Der Server prefetcht Daten in einen dehydratedState, der Client rehydriert ihn und vermeidet so Waterfalls.

Globale Standardwerte in der QueryClient-Konfiguration reduzieren Wiederholungen erheblich. staleTime: 60_000 bedeutet, dass alle Queries ihre Daten für eine Minute als frisch betrachten und keinen Background-Refetch auslösen. retry: 1 reduziert automatische Wiederholungsversuche auf einen – sinnvoll für produktionstaugliche Applikationen, wo endlose Retry-Loops die Serverinfrastruktur belasten. Die TanStack Query v5-Devtools sind ein unverzichtbares Debugging-Werkzeug und werden separat als @tanstack/react-query-devtools installiert.


// main.tsx — QueryClient setup with global defaults
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60_000,          // data is fresh for 60 seconds
      gcTime: 5 * 60_000,         // keep unused cache entries for 5 minutes
      retry: 1,                   // one retry on network errors
      refetchOnWindowFocus: true, // re-sync when tab becomes active
    },
    mutations: {
      retry: 0,                   // do not retry mutations automatically
    },
  },
});

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Router />
      {/* Only rendered in development builds */}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

3. useQuery: Daten abrufen mit Typsicherheit

Der useQuery-Hook ist die primäre API für alle Lesezugriffe. Er nimmt ein Konfigurationsobjekt mit queryKey und queryFn als Pflichtfelder. Der queryKey ist ein Array, das den Cache-Eintrag eindeutig identifiziert und gleichzeitig als Abhängigkeitsliste für automatisches Refetching dient – ähnlich dem Dependency-Array in useEffect, aber mit automatischem Cache-Management. Wenn sich ein Wert im queryKey ändert, startet TanStack Query automatisch einen neuen Request.

TypeScript-Generics bei useQuery<ResponseType, ErrorType> geben vollständige Typensicherheit: data ist typisiert, error hat den korrekten Typ. Die select-Option ermöglicht Datentransformationen direkt im Hook, ohne einen separaten Transformationsschritt. Das Memoization von select sorgt dafür, dass nachgelagerte Berechnungen nur neu ausgeführt werden, wenn sich die zugrundeliegenden Daten tatsächlich ändern. Der destructured Return des Hooks gibt alle relevanten Zustände: data, error, isPending, isError, isFetching und isStale.


// useProducts.ts — typed query hook with select transformation
import { useQuery } from "@tanstack/react-query";

interface Product {
  id: number;
  name: string;
  price: number;
  category: string;
  stock: number;
}

// Centralize query key factory for cache consistency
export const productKeys = {
  all: ["products"] as const,
  list: (filters: Record<string, unknown>) =>
    [...productKeys.all, "list", filters] as const,
  detail: (id: number) => [...productKeys.all, "detail", id] as const,
};

async function fetchProducts(category?: string): Promise<Product[]> {
  const url = category
    ? `/api/products?category=${encodeURIComponent(category)}`
    : "/api/products";
  const res = await fetch(url);
  if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
  return res.json();
}

export function useProducts(category?: string) {
  return useQuery({
    queryKey: productKeys.list({ category }),
    queryFn: () => fetchProducts(category),
    // Transform: only return in-stock items to subscriber
    select: (products) => products.filter((p) => p.stock > 0),
    staleTime: 30_000,
    placeholderData: (prevData) => prevData, // keep old data while reloading
  });
}

4. Caching-Strategien: staleTime und gcTime verstehen

Die zwei wichtigsten Zeitparameter von TanStack Query v5 werden häufig verwechselt. staleTime bestimmt, wie lange ein Cache-Eintrag als frisch gilt. Innerhalb dieses Zeitfensters lösen Komponenten, die denselben Query mounten, keinen Netzwerkrequest aus – sie erhalten die gecachten Daten sofort. Nach Ablauf der staleTime gelten die Daten als veraltet (stale), aber sie werden nicht sofort gelöscht. Stattdessen löst das nächste Triggerereignis (Window Focus, Komponenten-Mount, manuelle Invalidierung) einen Background-Refetch aus, während die alten Daten weiterhin angezeigt werden.

gcTime (früher cacheTime) bestimmt, wie lange ein unbenutzter Cache-Eintrag im Speicher gehalten wird, nachdem alle abonnierten Komponenten unmounten. Ein Query, der von keiner Komponente mehr verwendet wird, wird nach Ablauf der gcTime aus dem Cache entfernt und bei erneutem Bedarf frisch abgerufen. Der typische Anwendungsfall für hohe staleTime: Referenzdaten wie Kategorien oder Konfigurationswerte, die sich selten ändern. Für Benutzer-spezifische Daten wie den eigenen Warenkorb ist niedrige staleTime mit refetchOnWindowFocus: true die richtige Wahl.

5. useMutation: Schreiboperationen mit Fehlerbehandlung

Während useQuery für idempotente Lesezugriffe konzipiert ist, übernimmt useMutation alle schreibenden Operationen. Der wichtigste Unterschied: Mutations werden nicht automatisch ausgelöst, sondern durch einen expliziten mutate()- oder mutateAsync()-Aufruf. Die Callbacks onSuccess, onError und onSettled ermöglichen saubere Fehlerbehandlung und Cache-Invalidierung nach der Mutation. Das Muster onSuccess: () => queryClient.invalidateQueries() ist der Standardweg, um nach einer Schreiboperation betroffene Queries zu aktualisieren.

In TanStack Query v5 gibt es zwei Ebenen von Mutation-Callbacks: die im useMutation-Hook definierten (einmal pro Mutation-Instanz) und die im mutate()-Aufruf übergebenen (einmal pro Aufruf). Letztere ermöglichen komponentenspezifische Reaktionen, zum Beispiel das Weiterleiten auf eine Bestätigungsseite nach erfolgreicher Erstellung, während die Hook-Level-Callbacks für globale Aktionen wie Cache-Invalidierung zuständig sind. Der isPending-State der Mutation verhindert Doppelklicks und zeigt Ladeanimationen auf dem Submit-Button.


// useCreateOrder.ts — mutation with cache invalidation and error handling
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { orderKeys } from "./queryKeys";

interface CreateOrderPayload {
  productId: number;
  quantity: number;
  shippingAddress: string;
}

interface Order {
  id: number;
  status: "pending" | "confirmed" | "shipped";
  total: number;
}

async function createOrder(payload: CreateOrderPayload): Promise<Order> {
  const res = await fetch("/api/orders", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(payload),
  });
  if (!res.ok) {
    const err = await res.json();
    throw new Error(err.message ?? `HTTP ${res.status}`);
  }
  return res.json();
}

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

  return useMutation({
    mutationFn: createOrder,
    onSuccess: (newOrder) => {
      // Invalidate order list so it refetches in the background
      queryClient.invalidateQueries({ queryKey: orderKeys.all });
      // Directly populate the new order's detail cache entry
      queryClient.setQueryData(orderKeys.detail(newOrder.id), newOrder);
    },
    onError: (error: Error) => {
      console.error("[createOrder] failed:", error.message);
    },
  });
}

6. Optimistic Updates: sofortiges UI-Feedback

Optimistische Updates sind das Muster, bei dem die UI sofort auf eine Benutzeraktion reagiert, bevor die Serverantwort eintrifft. Das ergibt sich aus einer einfachen UX-Überlegung: In 95% der Fälle schlägt eine Mutation nicht fehl, und der Nutzer wartet unnötig. TanStack Query v5 bietet dafür einen strukturierten Workflow im onMutate-Callback: den aktuellen Cache-Zustand sichern, den Cache optimistisch aktualisieren, und im onError-Callback bei Fehlern den gespeicherten Zustand wiederherstellen.

Das onMutate-Pattern beginnt mit queryClient.cancelQueries(), um laufende Hintergrund-Refetches zu stoppen – andernfalls könnten veraltete Serverantworten den optimistischen Update überschreiben. Dann wird der aktuelle Stand mit queryClient.getQueryData() gespeichert und der Cache mit queryClient.setQueryData() optimistisch aktualisiert. Der gespeicherte Kontext wird an onError übergeben und bei Fehler mit queryClient.setQueryData() zurückgeschrieben. onSettled invalidiert schließlich den Query, damit die tatsächlichen Serverdaten geladen werden.

7. Prefetching und Query-Invalidierung

Prefetching ist die Technik, Daten zu laden bevor der Nutzer sie braucht. TanStack Query v5 bietet dafür queryClient.prefetchQuery(), das denselben Mechanismus wie useQuery nutzt, aber ohne eine Komponente zu mounten. Typische Einsatzpunkte: beim Hover über einen Link, beim Einblenden einer Tooltip-Vorschau oder beim Laden einer Route. Mit Next.js und dem Server-Rendering-Muster prefetcht der Server alle nötigen Daten vor dem HTML-Rendering, serialisiert den Cache mit dehydrate() und sendet ihn als JSON mit. Der Client rehydriert ihn mit HydrationBoundary und hat sofort alle Daten ohne Netzwerkrunde.

Query-Invalidierung mit queryClient.invalidateQueries() ist der sauberste Weg, nach Mutationen für Datenkonsistenz zu sorgen. Die Methode markiert matching Cache-Einträge als veraltet und löst einen Background-Refetch aus, wenn der Query aktiv subscribed ist. Der queryKey-Filter verwendet hierarchisches Matching: { queryKey: ['products'] } invalidiert alle Queries, deren Key mit ['products'] beginnt – also sowohl die Produktliste als auch einzelne Produktdetails. Das Query-Key-Factory-Muster (alle Keys zentral definiert) macht dieses hierarchische Matching präzise und wartbar.

8. Infinite Queries und Pagination

useInfiniteQuery ist die spezialisierte API für Pagination und infinite Scroll in TanStack Query v5. Im Gegensatz zu useQuery akkumuliert sie Serverantworten über mehrere Seiten hinweg im Cache. Die getNextPageParam-Funktion extrahiert aus jeder Serverantwort den Parameter für die nächste Seite – üblicherweise ein Cursor, ein Offset oder eine Seitennummer. fetchNextPage() löst den nächsten Request aus und hängt das Ergebnis an data.pages an, ohne vorherige Seiten zu überschreiben.

Das flache Rendering der Ergebnisse erfordert ein data.pages.flatMap(page => page.items), das alle Seiten in eine einzige Liste überführt. hasNextPage und isFetchingNextPage steuern den Scroll-Trigger: Ein Intersection Observer auf dem letzten Listenelement ruft fetchNextPage() auf, wenn es sichtbar wird. Für traditionelle Pagination mit expliziten Seitennummern empfiehlt sich stattdessen einfaches useQuery mit dem Seiten-Index im queryKey und placeholderData: (prevData) => prevData, um Blitzen zwischen Seitenübergängen zu vermeiden.

9. TanStack Query v5 vs. manuelle useEffect-Lösung

Der direkte Vergleich zeigt, warum der manuelle Ansatz mit useEffect für Server State mittelfristig immer scheitert und welche konkreten Probleme TanStack Query v5 löst.

Aspekt Manuell (useState + useEffect) TanStack Query v5 Unterschied
Doppelte Requests Pro Komponente ein Request Automatisch dedupliziert Kein extra Netzwerktraffic
Veraltete Daten Manuell nach Mutation neu laden Automatische Invalidierung Konsistenz ohne Aufwand
Background Sync Nicht vorhanden refetchOnWindowFocus Immer aktuelle Daten
Loading-Zustände 3+ separate useState-Flags status, isPending, isFetching Konsistente API, weniger Bugs
SSR / Prefetching Komplexe manuelle Lösung dehydrate / HydrationBoundary Kein Waterfall beim ersten Render

Der Vergleich ist nicht fair für simple Applikationen: Wer einen einzigen API-Endpunkt hat, der sich nie ändert, braucht kein TanStack Query. Ab dem Punkt, wo Daten aus mehreren Endpunkten kommen, Mutationen Cache-Invalidierungen benötigen oder SSR eine Rolle spielt, wird der manuelle Ansatz zur Quelle endloser Bugs. TanStack Query zahlt sich in diesem Kontext ab der zweiten komplexen Komponente aus.

Mironsoft

React Architektur, TanStack Query und skalierbare Frontend-Systeme

Server State, der nie mehr out of sync ist?

Wir migrieren bestehende React-Applikationen auf TanStack Query v5 – von der Query-Key-Architektur über Mutation-Strategien bis zur SSR-Integration mit Next.js oder Remix.

Code-Analyse

Bestehende useEffect-Datenabrufe analysieren und Migrationspfad auf TanStack Query v5 definieren

Query-Architektur

Query-Key-Factory, Caching-Strategie und Mutation-Muster für euer Datenmodell entwerfen

SSR-Integration

dehydrate/HydrationBoundary mit Next.js App Router oder Remix für waterfall-freies Rendering

10. Zusammenfassung

TanStack Query v5 ist heute der Standard für Server-State-Management in React-Applikationen. useQuery mit typisierter queryFn und Query-Key-Factory gibt vollständige Kontrolle über Caching und Refetching. staleTime und gcTime steuern, wie aggressiv oder konservativ die Bibliothek mit Netzwerkanfragen umgeht. useMutation mit onSuccess-Invalidierung hält Cache und Server synchron. Optimistische Updates machen Interaktionen für den Nutzer sofort spürbar. Prefetching und dehydrate eliminieren Waterfalls beim ersten Render.

Das Query-Key-Factory-Muster ist der Klebstoff, der alles zusammenhält: Alle Cache-Schlüssel werden zentral definiert, hierarchisch strukturiert und über das gesamte Projekt konsistent genutzt. Invalidierungen treffen genau die richtigen Einträge. Prefetches befüllen exakt den Cache, den Komponenten lesen werden. TypeScript-Generics stellen sicher, dass data und error immer die korrekten Typen tragen. Mit dieser Architektur skaliert Server State in React von einzelnen Datenabrufen bis zu komplexen Multi-Endpoint-Applikationen mit SSR.

TanStack Query v5 — Das Wichtigste auf einen Blick

Query Keys

Array-basiert, hierarchisch, zentral als Factory definiert. Änderungen im Key triggern automatisches Refetching – ähnlich useEffect-Dependencies.

staleTime vs. gcTime

staleTime: wie lange Daten als frisch gelten. gcTime: wie lange unbenutzter Cache gehalten wird. Beides separat konfigurierbar pro Query.

Mutations + Invalidierung

onSuccess: () => queryClient.invalidateQueries() ist das Standardmuster. Hierarchisches Matching invalidiert alle betroffenen Einträge auf einmal.

Optimistic Updates

onMutate: cancelQueries → getQueryData → setQueryData. onError: setQueryData zurück. onSettled: invalidateQueries für frische Serverdaten.

11. FAQ: TanStack Query v5 und Server State

1isPending vs. isLoading in v5?
isLoading existiert in v5 nicht mehr. isPending: kein gecachter Wert + Query läuft. isFetching: Fetch läuft, gecachte Daten ggf. bereits sichtbar.
2useQuery vs. useSuspenseQuery?
useSuspenseQuery integriert mit React Suspense. Komponente suspended beim Laden, Suspense Boundary zeigt Fallback. Vereinfacht Komponenten – data ist immer definiert.
3Tab-übergreifende Synchronisation?
broadcastQueryClient-Plugin sendet Invalidierungen via BroadcastChannel. Mutation in Tab A triggert Refetch in Tab B automatisch.
4TanStack Query ersetzt Redux?
Für Server State ja. Für Client State (UI, Formulare) bleibt Zustand/Redux sinnvoll. Kombination TanStack Query + Zustand deckt beide Bereiche ohne Überschneidung.
5Query deaktivieren?
enabled: false verhindert automatische Ausführung. enabled: term.length >= 3 startet Suche erst ab 3 Zeichen – klassisches Suchfeld-Muster.
6Mehrere Komponenten, derselbe Query?
Automatische Deduplizierung. Alle Subscriber mit gleichem queryKey teilen einen Request und einen Cache-Eintrag. Kein Mehrfach-Fetch.
7Next.js App Router Integration?
Server Component: prefetchQuery → dehydrate → HydrationBoundary-Prop. Client Component: useQuery mit gleichem Key = sofortige Daten ohne Waterfall.
8staleTime nach Datentyp wählen?
Referenzdaten: 5–60 Min. Benutzerdaten: 30–60 s. Echtzeit: 0 s mit refetchInterval. Globaler Default 60 s ist ein guter Startpunkt.
9Cache-Probleme debuggen?
ReactQueryDevtools zeigt alle Cache-Einträge, Status (fresh/stale/fetching), Observer-Count und Data-Preview. Effektivstes Werkzeug für Query-Debugging.
10TanStack Query ohne React?
Framework-agnostisch. Offizielle Adapter für Vue, Solid, Svelte, Angular. QueryClient und Caching sind framework-unabhängig. React-Hooks sind nur ein Adapter.