Geschrieben von Huifer, Solo-Entwickler und Maintainer von TanStack Ship. Die TanStack-Query-Best-Practices, die ich heute verwende, sind nicht diejenigen, mit denen ich angefangen habe. Meine erste TanStack-Query-Implementierung hatte Query-Keys wie
['getUser', userId],['fetchProduct']und['products']– eine Namenskonvention, die funktionierte, bis ich 30 Queries hatte und keine Ahnung, welche davon stale waren. Ich habe einen Tag damit verbracht, die Query-Key-Konvention über drei Apps hinweg zu standardisieren, und das war der Refactoring-Schritt mit dem höchsten Hebelwert im gesamten Jahr. Dieser Artikel ist die Konvention, die ich nun in jeder neuen App verwende.Verifizierte Quellen: TanStack Query documentation · TanStack Query best practices · TanStack Query GitHub Letzte Aktualisierung: 2026-09-18 · Changelog
TL;DR: Die drei TanStack-Query-Practices mit dem höchsten Hebelwert: (1) Query-Key-Konvention mit Punkten – ['users', userId, 'profile']; (2) staleTime auf das natürliche Aktualisierungsintervall der Daten setzen, nicht auf 0; (3) Kritische Routen beim Hover über Links vorladen. Alles andere ist Optimierung nach diesen drei Punkten.
Practice 1: Query-Key-Konvention
Die wichtigste Konvention in einer TanStack-Query-App ist der Query-Key. Query-Keys bestimmen die Cache-Identität – zwei Queries mit demselben Key teilen sich dieselben gecachten Daten. Eine schlechte Konvention macht Cache-Invalidierung unberechenbar. Eine gute Konvention macht sie offensichtlich.
Die punktgetrennte Konvention
// Jeder Query-Key folgt dem Schema: [entity, id?, subResource?]
// Das macht Invalidierung vorhersehbar und Keys vorhersehbar
// Einzelne Ressource nach ID
['users', userId]
['products', productId]
['orders', orderId]
// Unterressourcen einer einzelnen Entität
['users', userId, 'profile']
['users', userId, 'orders']
['products', productId, 'reviews']
// Collections (ohne ID)
['users']
['products']
['orders']
// Gefilterte Collections
['products', 'category', categorySlug]
['orders', 'status', 'pending']
Diese Konvention hat eine Regel: Entitätsnamen sind Plural, IDs sind Singular, Unterressourcen sind Singular. ['users', '123'] ist ein User. ['users', '123', 'orders'] sind die Orders dieses Users.
Warum das für die Invalidierung wichtig ist
Mit dieser Konvention ist die Invalidierung vorhersehbar:
// Alle Queries für einen bestimmten User invalidieren
queryClient.invalidateQueries({ queryKey: ['users', userId] })
// Invalidiert: ['users', userId], ['users', userId, 'profile'], ['users', userId, 'orders']
// Alle Queries für die Reviews eines bestimmten Produkts invalidieren
queryClient.invalidateQueries({ queryKey: ['products', productId, 'reviews'] })
// Alle Queries für einen Ressourcentyp invalidieren
queryClient.invalidateQueries({ queryKey: ['products'] })
// Invalidiert: ['products'], ['products', 'category', ...], ['products', productId, ...]
Ohne Konvention wissen Sie nie, welche Queries von einer Invalidierung betroffen sind. Mit der Konvention sagt Ihnen die Query-Key-Struktur genau, was passiert.
Practice 2: staleTime, Nicht 0
Der Standardwert staleTime: 0 bedeutet, dass jede Query bei jedem Mount neu geladen wird. Für Daten, die sich häufig ändern (Benachrichtigungen, Live-Metriken), ist das korrekt. Für Daten, die sich selten ändern (User-Profil, Produktkatalog), ist das eine Performance-Steuer.
// ❌ Standard: Neu laden bei jedem Mount – teuer
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
// staleTime beträgt standardmäßig 0
})
// ✅ staleTime auf das natürliche Aktualisierungsintervall der Daten setzen
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 1000 * 60 * 5, // 5 Minuten – User-Profil ändert sich selten
})
// ✅ Statische Referenzdaten: nie neu laden, außer explizit invalidiert
const { data: countries } = useQuery({
queryKey: ['reference', 'countries'],
queryFn: () => fetchCountries(),
staleTime: 1000 * 60 * 60 * 24, // 24 Stunden
})
Das mentale Modell: staleTime gibt an, wie lange Sie bereit sind, stale Daten auszuliefern. Für User-Profil-Daten sind 5 Minuten in Ordnung. Für einen Aktienkurs: 0 Sekunden. Für eine Länderliste: Unendlichkeit.
Practice 3: Prefetch bei Link-Hover
Das UX-Pattern mit der höchsten Wirkung in TanStack Query: Daten vorladen, wenn der User über einen Link hovert. Die Daten sind im Cache, sobald er klickt.
import { useQueryClient } from '@tanstack/react-query'
import { Link } from 'react-router-dom'
function ProductList({ products }) {
const queryClient = useQueryClient()
return (
<div className="product-grid">
{products.map(product => (
<Link
key={product.id}
to={`/products/${product.id}`}
onMouseEnter={() => {
// Prefetch bei Hover – Daten sind bereit, bevor geklickt wird
queryClient.prefetchQuery({
queryKey: ['products', product.id],
queryFn: () => fetchProduct(product.id),
staleTime: 1000 * 60 * 5,
})
}}
>
<ProductCard product={product} />
</Link>
))}
</div>
)
}
Das Ergebnis: Ein Klick auf eine Produktkarte rendert die Detailseite sofort – die Query-Daten sind bereits im Cache. Das onMouseEnter feuert vor der Navigation und gibt Ihnen 50–200 ms Prefetch-Zeit.
Practice 4: Globale QueryClient-Standards
Setzen Sie globale Standards, die zu den Aktualisierungscharakteristika Ihrer Daten passen, nicht zu Randfällen:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// Standard: 2 Minuten staleTime
// Das ist der richtige Standard für die meisten SaaS-Daten
staleTime: 1000 * 60 * 2,
// gcTime: 10 Minuten
// Daten bleiben 10 Minuten nach dem letzten Subscriber im Speicher
gcTime: 1000 * 60 * 10,
// 3 Versuche mit exponentiellem Backoff
retry: 3,
retryDelay: (attemptIndex) =>
Math.min(1000 * 2 ** attemptIndex, 30000),
// Bei Window-Focus neu laden für authentifizierte User
// (Daten könnten sich geändert haben, während der Tab inaktiv war)
refetchOnWindowFocus: true,
// Nicht neu laden, wenn bereits ein Ladevorgang läuft
refetchOnMount: false,
},
mutations: {
// Standard-Retry für Mutations: 0
// Mutations sollten laut fehlschlagen, nicht still erneut versuchen
retry: 0,
},
},
})
Practice 5: SSR-Hydration mit v5
Für SSR-Apps hat sich das Hydration-Pattern in v5 geändert. Stellen Sie sicher, dass Server- und Client-State übereinstimmen:
// Server: Prefetch mit konsistenter staleTime
// (muss mit der staleTime des Clients übereinstimmen, damit v5-Hydration funktioniert)
const dehydratedState = dehydrate(
new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5,
gcTime: 1000 * 60 * 10,
},
},
})
)
// Client: staleTime des Servers in der QueryClient-Konfiguration matchen
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
// Muss mit der staleTime des Servers übereinstimmen für korrekte Hydration
staleTime: 1000 * 60 * 5,
gcTime: 1000 * 60 * 10,
},
},
})
)
Die v5-Verbesserung: Wenn die Server-Daten frisch sind (innerhalb von staleTime), verwendet der Client sie direkt, ohne ein erneutes Laden. Abweichende staleTime-Werte zwischen Server und Client verursachen lautlose Neuabrufe bei der Hydration.
Practice 6: Background-Refetch-Strategie
Laden Sie nicht bei jedem Focus-Event alles neu. Verwenden Sie staleTime als primäre Refetch-Kontrolle und nutzen Sie Background-Refetch sparsam:
const { data, isFetching, isStale } = useQuery({
queryKey: ['dashboard-metrics'],
queryFn: () => fetchDashboardMetrics(),
staleTime: 1000 * 60, // 1 Minute
// Nur neu laden, wenn die Query aktuell stale IST UND das Fenster Fokus hat
refetchOnWindowFocus: (query) => query.state.data !== undefined,
})
Die Funktionsform von refetchOnWindowFocus: nur neu laden, wenn wir bereits Daten haben. Das verhindert das UX-Anti-Pattern „Ladezustand bei jedem Tab-Wechsel".
Die 5-Punkte-Produktions-Checkliste
- Query-Key-Konvention dokumentiert – jedes Teammitglied verwendet dieselbe Struktur
- staleTime pro Query gesetzt – 0 ist die Ausnahme, nicht der Standard
- Kritische Routen vorgeladen – Produktdetail- und User-Profil-Seiten bei Link-Hover
- SSR-staleTime stimmt mit Client überein – führen Sie bei jeder SSR-Änderung einen Hydration-Smoke-Test durch
- QueryClient-gcTime beträgt mindestens 10 Minuten – zu niedrig und die Rücknavigation fühlt sich langsam an
TanStack Ship wird mit TanStack Query v5 ausgeliefert – mit vordefinierter Query-Key-Konvention und SSR-Hydration. Weitere Informationen finden Sie im TanStack Query v5 Guide und in der vollständigen Feature-Liste.