TanStack Query Server State Management: Der komplette Produktions-Guide 2026

Dieser komplette Guide 2026 für das Server State Management mit TanStack Query zeigt Ihnen getypte Query-Key-Factorys, Optimistic Updates und Patterns.

Huifer
Huifer
1. Juli 202611 min read
Auch verfügbar auf:English · 中文

Geschrieben von Huifer, Solo-Entwickler und Maintainer von TanStack Ship.

Ich habe das Admin-Panel von TanStack Ship mit TanStack Query v5 gebaut und einen 4-stündigen Stale-MRR-Vorfall durchlebt, bevor ich gelernt habe, Query-Keys wie ein Datenbank-Schema zu behandeln. Dieser Guide ist die Produktionsversion dessen, was ich gerne an Tag eins gelesen hätte — getypte Key-Factorys, Server Functions, Optimistic Updates, Streaming und das ehrliche Framework zur Entscheidungsfindung im Vergleich zu Redux Toolkit Query und SWR. Alles hier läuft in Produktion auf Cloudflare Workers + D1 unter tanstackship.com.

Hinweis zur materiellen Verbindung: Ich bin der Autor und Maintainer von TanStack Ship, einem in diesem Artikel erwähnten kommerziellen Boilerplate. TanStack Ship wird unter einer Lifetime-Lizenz verkauft. Wo dieser Artikel TanStack Query mit Alternativen (RTK Query, SWR) vergleicht, ist der Vergleich rein technischer Natur und gilt unabhängig davon, ob Sie sich für TanStack Ship entscheiden. Wo ich TanStack Ship explizit empfehle, empfehle ich mein eigenes Produkt — berücksichtigen Sie dies bei der Gewichtung der Empfehlung und bewerten Sie die technischen Vorzüge unabhängig. Preisdetails finden Sie auf der Pricing-Seite.

Verifizierte Quellen: TanStack Query v5 docs · TanStack Start Server Functions · Cloudflare Workers docs · Mein Stale-Data-Postmortem

Zuletzt aktualisiert: 01. Juli 2026 · Changelog


TL;DR

Der Server-State — alles, was in Ihrer Datenbank, in der API von Drittanbietern oder im Service eines anderen Teams lebt — ist der am schwersten zu verwaltende Zustand in einer modernen Web-App, da er sich außerhalb Ihres React-Baumes befindet. TanStack Query ist im Jahr 2026 die produktionsreife Antwort: Es verwaltet den Cache, den Staleness-Vertrag und den Lifecycle für Mutations und Invalidierungen. Dieser Guide ist die komplette Produktionsversion: warum der Server-State so anders ist, die Architektur, die TanStack Query Ihnen bietet, die getypte Query-Key-Factory, die ich in TanStack Ship nutze, Mutations + Optimistic Updates + Streaming + Pagination sowie die ehrliche Entscheidungsfindung im Vergleich zu RTK Query und SWR. Wenn Sie im Jahr 2026 ein SaaS-Produkt ausliefern und TanStack Query (oder etwas Vergleichbares) nicht verwenden, schreiben Sie eine Infrastruktur neu, um die Sie sich eigentlich nicht kümmern müssten.


Warum das Server State Management der schwierigste Teil von moderner SaaS ist

In jeder React-App, die ich ausgeliefert habe, fällt der Zustand in drei Kategorien — und nur eine davon ist wirklich schwierig.

Die drei Kategorien von State

  • Client state: UI-Toggles, Formularentwürfe, Modal geöffnet/geschlossen, Theme-Präferenzen. Lebt in Ihrem React-Baum. Einfach. useState, Zustand oder React Context kümmern sich darum.
  • URL state: Filter, Pagination, Suchanfragen. Lebt in der Adressleiste. Einfach. Search Params kümmern sich darum.
  • Server state: Abonnements, MRR-Zahlen, Feature-Flags, API-Antworten von Drittanbietern. Lebt in einer Datenbank, die Sie nicht kontrollieren, hinter einem Netzwerk, das Ihnen nicht gehört, mit einer TTL, die Sie nicht gewählt haben. Schwierig.

Das „schwierige“ an der Sache sind nicht die Daten — es ist der Lifecycle. Der Server-State verfügt über einen Cache, ein Staleness-Fenster, eine Fetch-Richtlinie, eine Error-Retry-Richtlinie, eine Deduplizierungsrichtlinie und eine Mutation-Invalidation-Richtlinie. Ihre Komponente besitzt all das nicht. Das Netzwerk besitzt es.

Warum der Server-State anders ist — Er lebt außerhalb Ihrer App

Der klassische Fehler ist, Server-Daten wie Client-Daten zu behandeln. Ich habe das zwei Jahre lang getan. Das Pattern sieht wie folgt aus:

typescript
// Das naive Pattern — NICHT so ausliefern
const [users, setUsers] = useState<User[]>([])
const [loading, setLoading] = useState(false)

useEffect(() => {
  setLoading(true)
  fetch('/api/users').then(r => r.json()).then(setUsers)
  // Was ist mit Caching? Staleness? Retrys? Deduplizierung?
  // Was ist, wenn zwei Komponenten gleichzeitig dieselben Daten laden?
  // Was ist, wenn der User die Seite verlässt und zurückkehrt?
  // Was ist mit Offline? Was ist mit Focus-Refetching?
}, [])

Jeder Entwickler löst dies von Grund auf neu. Dann lösen sie es beim nächsten Projekt wieder. Dann lösen sie das Stale-Data nach Mutation-Problem. Dann lösen sie das Daten-Duplizierung bei schnellem Mount/Unmount-Problem. Dann lösen sie das Race-Condition, wenn eine langsame Antwort nach einer schnellen eintrifft-Problem. Im dritten Jahr haben Sie 4.000 Zeilen Caching-Code und ein Postmortem wie das, das ich im März 2026 veröffentlicht habe, wo 12% der Admin-Nutzer 4 Stunden und 17 Minuten lang einen veralteten MRR sahen.

TanStack Query existiert, weil jedes React-Team, das ein echtes Produkt ausliefert, dies irgendwann nachbaut. Sie können absolut sechs Monate damit verbringen, es selbst zu entwickeln, oder Sie nutzen die Library, die sich bereits über Millionen von Produktiv-Deployments hinweg bewährt hat. Ich habe mich für die Library entschieden.


TanStack Query als Server-State-Layer — Was es tatsächlich tut

TanStack Query ist kein „Fetch-Wrapper“. Es ist eine Server-State-Maschine. Der Cache ist ein First-Class-Objekt. Zeit ist eine First-Class-Dimension. Mutations bilden einen First-Class-Lifecycle. Dies zu verstehen, ändert Ihre Sichtweise darauf.

Der Cache als First-Class-Objekt

Wenn Sie useQuery({ queryKey: ['users', userId], queryFn: fetchUser }) aufrufen, speichert TanStack Query das Ergebnis in einem globalen, strukturierten Cache, basierend auf dem von Ihnen übergebenen Array. Zwei Komponenten, die dasselbe Key-Array anfordern, teilen sich einen einzigen Netzwerkrequest. Zwei Komponenten, die unterschiedliche Keys anfordern, erhalten ihre eigenen Slots. Der Cache verfügt über einen Lifecycle — fresh, stale, inactive, deleted — und Sie konfigurieren ihn mit staleTime, gcTime und refetchInterval.

typescript
// Die Struktur, die tatsächlich in der Produktion funktioniert
const { data, isPending, isError, isFetching } = useQuery({
  queryKey: ['subscriptions', userId, 'list'],
  queryFn: () => fetchSubscriptions(userId),
  staleTime: 30_000,        // 30s bevor dies als stale gilt
  gcTime: 5 * 60_000,       // 5 Min. bevor inaktiver Cache vom Garbage Collector entfernt wird
  refetchOnWindowFocus: true,
  retry: 3,
})

Das erste Mal, wenn Sie staleTime als bewusste Entscheidung und nicht als versteckten Standardwert betrachten, erkennen Sie, dass die Library von Ihnen verlangt, über die Zeit als integralen Teil Ihres Datenvertrags nachzudenken. Das ist das richtige mentale Modell. Der Cache ist ein Vertrag.

Stale vs. Fresh Data — Der Staleness-Vertrag

„Fresh“ bedeutet, dass auf die Daten so wie sie sind vertraut wird — kein Refetch beim Mount. „Stale“ bedeutet, dass die Daten wahrscheinlich noch gut sind, aber beim Mount oder erneuten Focus revalidiert werden sollten. Das ist das gesamte mentale Modell. Die Library fetcht standardmäßig nicht aggressiv; sie überlässt Ihnen die Entscheidung. Für einen Aktien-Ticker benötigen Sie staleTime: 0 und aggressives Refetching. Für ein Config-Panel möchten Sie staleTime: Infinity und eine Invalidierung ausschließlich bei einer Mutation.

In TanStack Ship verwendet das MRR-Dashboard staleTime: 60_000, da Umsatzdaten zwar sehr leselastig sind, die Aktualisierungen jedoch durch Subscription-Events und nicht kontinuierlich erfolgen. Die Admin-Benutzerliste verwendet staleTime: 0, weil Admin-Aktionen sofortige Konsistenz erfordern. Die Entscheidung wird pro Query und nicht pro App getroffen.

Mutations, Invalidation und der Query-Key-Vertrag

Mutations sind das dritte First-Class-Konzept. useMutation führt nicht automatisch eine Invalidierung durch — Sie entscheiden, was wann invalidiert wird. Dies ist der Aspekt, bei dem jedes Team, mit dem ich bisher gearbeitet habe, einen Bug produziert hat, da Query-Keys Arrays sind und invalidateQueries anhand eines Präfixes zuordnet.

typescript
// Das ist das Bug-Pattern, das ich im März 2026 ausgeliefert habe
const applyCredits = useMutation({
  mutationFn: (input) => fetch('/api/credits', { method: 'POST', body: JSON.stringify(input) }),
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ['subscriptions'] })
    // Aber die Admin-MRR-Query verwendet ['admin', 'mrr', 'global']
    // — kein gemeinsames Präfix → nie invalidiert → 4 Stunden lang veralteter MRR
  },
})

Die Lösung ist eine getypte Query-Key-Factory. Ich zeige Ihnen nun das genaue Pattern, das ich in TanStack Ship ausliefere. Zunächst jedoch ein Blick auf die umfassendere Architektur.


Die Architektur, die ich bei TanStack Ship ausliefere — Production-Patterns

TanStack Ship basiert auf TanStack Start + Cloudflare Workers. Das Data-Layer jedes Moduls folgt denselben vier Patterns: getypte Query-Key-Factory, Server Function-Integration, Optimistic Updates mit Rollback und explizites Streaming/Pagination. Nichts davon ist exotisch. Alle sind verpflichtend.

Getypte Query-Key-Factory

Die Factory ist die wichtigste Datei in jeder TanStack Query-Codebase. Sie erzwingt Namenskonventionen, bietet typsichere Key-Generierung und verhindert den Scope-Collision-Bug, den ich in der Produktion ausgeliefert hatte. Die vollständige Datei von TanStack Ship sieht so aus:

typescript
// src/lib/query-keys.ts — die getypte Factory in TanStack Ship
export const queryKeys = {
  subscriptions: {
    all: ['subscriptions'] as const,
    list: (userId: string) => ['subscriptions', 'list', userId] as const,
    detail: (userId: string, subId: string) =>
      ['subscriptions', 'detail', userId, subId] as const,
  },
  admin: {
    all: ['admin'] as const,
    mrr: {
      global: () => ['admin', 'mrr', 'global'] as const,
      byPlan: (planId: string) => ['admin', 'mrr', 'byPlan', planId] as const,
    },
  },
  billing: {
    all: ['billing'] as const,
    invoices: (userId: string) => ['billing', 'invoices', userId] as const,
  },
} as const

Jedes useQuery und jeder invalidateQueries-Aufruf durchläuft diese Factory. Nach dem Vorfall im März 2026 habe ich einen CI-Lint hinzugefügt, der fehlschlägt, falls ein statischer String-Literal für Query-Keys außerhalb von query-keys.ts vorkommt. Der 40-Zeilen-Test ersetzte unzählige Stunden an Fehlersuche. Wenn Sie keine getypte Factory nutzen, sind Sie nur einen Refactor vom exakt selben Vorfall entfernt.

Server Functions + Query (native TanStack Start-Integration)

TanStack Start bietet eine native Brücke zwischen Server Functions und Query. Eine Server Function ist lediglich eine asynchrone Funktion auf dem Server; TanStack Start verpackt sie derart, dass der Client sie so aufrufen kann, als wäre sie lokal. Dies erübrigt die gesamte Frage nach dem "Format meiner API" — Ihre Server Function IST die API.

typescript
// src/lib/server/admin.ts — die Server Function, die das Admin-MRR-Widget aufruft
export const fetchAdminMRR = createServerFn({ method: 'GET' }).handler(
  async ({ context }) => {
    const session = await context.auth.getSession()
    if (!session?.user?.isAdmin) throw new Error('forbidden')
    return context.db
      .select({ total: sum(subscriptions.amountMonthly) })
      .from(subscriptions)
      .where(eq(subscriptions.status, 'active'))
  }
)

// Der Query-Layer — drei Zeilen, kein Fetch-Boilerplate
const { data: mrr } = useQuery({
  queryKey: queryKeys.admin.mrr.global(),
  queryFn: () => fetchAdminMRR(),
  staleTime: 60_000,
})

Achten Sie darauf, was fehlt: kein fetch, kein useEffect, keine manuelle Cache-Verwaltung. Die Server Function ist die API. Die Factory ist der Key. TanStack Query ist der Lifecycle. Cloudflare Workers führt die Funktion am Edge in meinen Tests im Free-Tier in ungefähr 40–90 ms beim Cold-Start aus. Dem Browser ist all das völlig egal.

Optimistic Updates und Rollback

Optimistic Updates machen Ihr SaaS gefühlt augenblicklich schnell. Das Pattern: Übernehmen Sie die Änderung im Cache, bevor der Server sie bestätigt, und führen Sie dann entweder den Commit (Erfolg) oder einen Rollback (Fehler) durch. Das Set aus onMutate / onError / onSettled in TanStack Query ist die kanonische Implementierung dafür.

typescript
const updatePlan = useMutation({
  mutationFn: (input: UpdatePlanInput) => updatePlanServerFn({ data: input }),
  onMutate: async (input) => {
    await queryClient.cancelQueries({ queryKey: queryKeys.admin.mrr.global() })
    const previous = queryClient.getQueryData(queryKeys.admin.mrr.global())
    queryClient.setQueryData(queryKeys.admin.mrr.global(), (old) =>
      applyOptimistic(old, input)
    )
    return { previous }
  },
  onError: (_err, _input, ctx) => {
    if (ctx?.previous) {
      queryClient.setQueryData(queryKeys.admin.mrr.global(), ctx.previous)
    }
  },
  onSettled: () => {
    queryClient.invalidateQueries({ queryKey: queryKeys.admin.mrr.global() })
  },
})

Die Kern-Erkenntnis: Der Rollback-Snapshot (ctx.previous) ist genau das, was Optimistic Updates sicher macht. Wenn die Mutation fehlschlägt, müssen Sie sich den vorherigen Status nicht extra merken — das Context-Objekt hat ihn bereits. Überspringen Sie den Snapshot, und Sie haben eine UX gebaut, in der das UI bei einem Fehler permanent lügt.

Streaming, Pagination und Infinite Queries

Für große Listen — Rechnungshistorien, Benutzerprotokolle, Activity-Feeds — ist useInfiniteQuery die Antwort. Es verwaltet einen paginierten Cache und hängt neue Seiten an, während der User scrollt. Kombinieren Sie das mit renderToStream aus TanStack Start für echtes Streaming-SSR. Das Resultat ist eine 10.000-Zeilen-Rechnungsliste, die die ersten 100 Zeilen in rund 150 ms zeichnet, während der Rest im Hintergrund hereinströmt. Genau das nutze ich im Rechnungs-Viewer des Admin-Bereichs von TanStack Ship für zahlende Kunden, deren Rechnungshistorien oft 5.000 Einträge übersteigen.

Für Pagination mit Einmallesevorgang ("One-shot paginated reads", z. B. Admin-Tabellen mit expliziten Seitenzahlen) bietet placeholderData: keepPreviousData die extrem flüssige UX "Vorherige Daten werden angezeigt, während die nächste Seite lädt". Das ist dieses feine Detail, das ein $10/Monat-Boilerplate von einem für $200/Monat unterscheidet.


Entscheidungs-Framework — Wann TanStack Query vs. Alternativen

Ich werde das monatlich gefragt: Warum TanStack Query anstelle von Redux Toolkit Query oder SWR? Die ehrliche Antwort: Beide haben echte Stärken. So denke ich darüber.

TanStack Query vs. RTK Query

Die Stärke von RTK Query ist die nahtlose Integration mit Redux-Stores auf Bundle-Ebene. Wenn Sie bereits eine Redux-basierte App mit signifikantem Client-State betreiben, liefert Ihnen RTK Query einen einzigen Cache, eine DevTools-Instanz und ein einheitliches mentales Modell. Das ist in der Realität stark.

TanStack Query gewinnt, wenn Sie ein eigenständiges Server-State-Layer wünschen, das kein Redux erfordert, wenn Sie First-Class Streaming und Infinite-Queries wollen und wenn sich Ihr Team nicht ohnehin tief in die Formalitäten von Redux eingearbeitet hat. TanStack Ship wird mit null Redux ausgeliefert — jedes Modul verwendet TanStack Query direkt. Weniger Boilerplate, weniger Konzepte zum Einstudieren.

TanStack Query vs. SWR

Die Stärke von SWR ist seine Einfachheit. Die API-Oberfläche ist kleiner. Wenn Ihre App lediglich 4–6 einfache Queries ohne Optimistic Updates, ohne Pagination und ohne Streaming besitzt, ist SWR schlichtweg weniger Code.

TanStack Query gewinnt in dem Moment, in dem Sie tiefere Funktionen wie Optimistic Updates mit Rollback, Infinite Queries, eine Streaming-SSR-Integration oder aussagekräftige DevTools benötigen. SWR holt zwar leise weiter auf, aber 2026 spricht die Ergonomie für komplexe SaaS-Apps immer noch sehr deutlich für TanStack Query. In meiner Liste der besten SaaS-Boilerplates im Jahr 2026 läuft jede Lösung, die auf dem TanStack aufbaut, mit TanStack Query.

Wann Sie TanStack Query NICHT verwenden sollten

  • Überhaupt kein Netzwerk (reine Client-Side Apps): Verwenden Sie useState / Zustand. Sie würden andernfalls für Cache-Mechanismen zahlen, die Sie nicht brauchen.
  • Nur ein Endpoint, nur eine Seite: Ein handgeschriebenes useEffect ist schneller programmiert. Greifen Sie erst zur TanStack Query, sobald der zweite Server-Request kommt.
  • Sie setzen ohnehin bereits voll auf Redux und möchten keinen zweiten Cache einführen: Bleiben Sie bei RTK Query. Fragmentieren Sie Ihr State-Layer nicht.

Für alles andere — und ganz besonders für jedwedes SaaS-Projekt, das Abrechnung, Dashboards, Admin-Panels oder die API-Integration von Drittanbietern umfasst — ist TanStack Query heutzutage der Standard. Seit 2024 habe ich kein einziges SaaS mehr ohne ausgeliefert.


Was TanStack Ship Out of the Box mitliefert

Der ganze Sinn von TanStack Ship liegt exakt darin, dass Sie am allerersten Tag genau keine der obigen Entscheidungen treffen müssen. Das Boilerplate wird mit vollständig integriertem TanStack Query geliefert, die getypten Query-Key-Factorys sind bereits geschrieben, die Server Functions sind vordefiniert und alle 14 Module greifen von Anfang an auf denselben Cache zu.

14 Module, Query-Key-Factory fest verdrahtet

Jedes Modul — Abonnements, Abrechnung, Admin, MRR-Dashboard, Feature-Flags, Content, E-Mail, UTM-Zuweisung, Affiliate-Tracking, Gutschein-Engine, Credit-System, Waitlist, Changelog, AI-Agenten-Skills — nutzt das gleiche getypte Factory-Pattern. Fügen Sie ein neues Modul hinzu, so ist die Factory die erste Datei, die Sie anpassen. Die CI warnt Sie umgehend durch einen Lint-Fehler, sollten Sie die Factory einmal vergessen. Die Lektion aus dem März 2026-Zwischenfall wurde in das Boilerplate einprogrammiert, sodass Sie diese nicht mehr auf die harte Tour lernen müssen.

MRR-Dashboard, Admin-Panels, Echtzeitdaten — alles über Query

Das MRR-Dashboard, die Admin-Benutzerliste, die Abo-Tabelle, die Rechnungsansicht, das Credit-Konto, das Feature-Flag-Panel — absolut alle sind useQuery-Konsumenten, die demselben strengen Staleness-Vertrag folgen: Fresh für 30–60 Sekunden, Invalidierung bei der zuständigen Mutation, Garbage-Collection nach exakt 5 Minuten Inaktivität. Das Cloudflare Workers + D1 Backend garantiert Ihnen in der Produktion blitzschnelle Cold-Starts an der Edge. Der TanStack Start Router gibt Ihnen dabei eine Formsicherheit, die bereits zur Build-Zeit wegbricht und nicht erst in der Produktion Fehler wirft.

Wenn man heute Boilerplates beurteilt, so lautet die Frage nicht: "Enthält es ein Dashboard?" — das tut fast jedes brauchbare. Die Frage lautet: "Was passiert, wenn die Daten auf dem Dashboard veraltet sind?" Genau diese Frage beantwortet die Architektur von TanStack Ship. Lifetime-Lizenz, 14 produktiv nutzbare Module, voller Quellcode-Zugriff, 14-tägige Rückerstattungsgarantie. Ausgeliefert mit genau jener getypten Factory, damit Ihnen mein Postmortem vom März 2026 in Ihrer Codebase nicht nochmal passiert.


Sehen Sie es im Live-Einsatz

Wenn Sie im Jahr 2026 ein SaaS bauen, ist die Entscheidung zum Server-State die wichtigste Weichenstellung für Ihre gesamte Architektur. Treffen Sie die beste Entscheidung für das Layer, rollen Sie konsequent die Factory mit aus und verfluchen Sie nie mehr nachts um 2 Uhr ein veraltetes MRR-Dashboard. Das ist der ganze Job.