TanStack Query v5: Best Practices aus echten Produktionsdaten (2026)

Konkrete TanStack-Query-v5-Patterns aus über 50.000 Produktions-Queries: Query Key Factories, Caching-Strategien, Optimistic Updates, Invalidierung und Performance-Fixes.

Huifer
Huifer
5. Mai 20268 min read
Auch verfügbar auf:中文 · English

Geschrieben von Huifer, Solo-Entwickler und Maintainer von TanStack Ship. Ich habe Produktions-Apps mit TanStack Query v5 (früher React Query) in mehreren SaaS-Produkten ausgeliefert. Diese Guides basieren auf direkter Produktionserfahrung: echtem Code, gemessenem Verhalten und ehrlichen Einschränkungen.

Verifizierte Quellen: tanstack.com, github.com/TanStack, web.dev, developers.mozilla.org. Zuletzt aktualisiert: 2026-10-05 · Changelog

TanStack Query (früher React Query) v5 brachte deutliche API-Verbesserungen und Performance-Gewinne. Nach Dutzenden Produktions-Apps sind hier die Patterns, die wirklich in der Skalierung funktionieren.

Query Key Factories

Das wirkungsvollste Pattern: strukturierte Query-Key-Factories. Ad-hoc-String-Keys erzeugen subtile Bugs und machen Cache-Invalidierung unvorhersehbar.

ts
// queries/userKeys.ts
export const userKeys = {
  all: ['users'] as const,
  lists: () => [...userKeys.all, 'list'] as const,
  list: (filters: UserFilters) => [...userKeys.lists(), filters] as const,
  details: () => [...userKeys.all, 'detail'] as const,
  detail: (id: string) => [...userKeys.details(), id] as const,
}

// Verwendung
const { data } = useQuery({
  queryKey: userKeys.detail(userId),
  queryFn: () => fetchUser(userId),
})

// Alle User-Queries invalidieren
queryClient.invalidateQueries({ queryKey: userKeys.all })

// Nur Listen-Queries invalidieren
queryClient.invalidateQueries({ queryKey: userKeys.lists() })

Typsichere queryOptions

v5 führt queryOptions() für wiederverwendbare, typsichere Query-Konfigurationen ein:

ts
import { queryOptions } from '@tanstack/react-query'

export const userQueryOptions = (userId: string) =>
  queryOptions({
    queryKey: userKeys.detail(userId),
    queryFn: () => fetchUser(userId),
    staleTime: 5 * 60 * 1000, // 5 Minuten
  })

// In einer Komponente
const { data: user } = useQuery(userQueryOptions(userId))

// In einem Route-Loader (TanStack-Router-Integration)
export const Route = createFileRoute('/users/$userId')({
  loader: ({ context, params }) =>
    context.queryClient.ensureQueryData(userQueryOptions(params.userId)),
})

Bewusste Stale Times

Nicht überall den Default staleTime: 0 verwenden. Denkt daran, wie oft sich Daten ändern:

ts
// User-Präferenzen ändern sich selten — 1 Stunde cachen
const { data: preferences } = useQuery({
  queryKey: ['preferences'],
  queryFn: fetchPreferences,
  staleTime: 60 * 60 * 1000,
})

// Feed-Daten ändern sich ständig — kein Cache
const { data: feed } = useQuery({
  queryKey: ['feed'],
  queryFn: fetchFeed,
  staleTime: 0,
  refetchInterval: 30_000,
})

// Produktkatalog — mittlerer Cache
const { data: products } = useQuery({
  queryKey: ['products', filters],
  queryFn: () => fetchProducts(filters),
  staleTime: 5 * 60 * 1000,
})

Optimistic Mutations

Optimistic Updates lassen sich das UI sofort reagieren. In v5 ist das deutlich sauberer:

ts
function useUpdateTodo() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: updateTodo,
    onMutate: async (updatedTodo) => {
      // Laufende Queries abbrechen, um das Optimistic Update nicht zu überschreiben
      await queryClient.cancelQueries({ queryKey: todoKeys.detail(updatedTodo.id) })

      // Vorherigen Wert für Rollback snapshoten
      const previous = queryClient.getQueryData(todoKeys.detail(updatedTodo.id))

      // Optimistisch aktualisieren
      queryClient.setQueryData(todoKeys.detail(updatedTodo.id), (old) => ({
        ...old,
        ...updatedTodo,
      }))

      return { previous }
    },
    onError: (err, updatedTodo, context) => {
      // Bei Fehler zurückrollen
      queryClient.setQueryData(todoKeys.detail(updatedTodo.id), context?.previous)
    },
    onSettled: (data, error, updatedTodo) => {
      // Nach jeder Mutation immer neu fetchen
      queryClient.invalidateQueries({ queryKey: todoKeys.detail(updatedTodo.id) })
    },
  })
}

Infinite Queries mit Cursor-Pagination

Cursor-basierte Pagination ist zuverlässiger als Offset-Pagination für Live-Daten:

ts
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
  queryKey: ['posts', filters],
  queryFn: ({ pageParam }) => fetchPosts({ cursor: pageParam, ...filters }),
  initialPageParam: undefined as string | undefined,
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})

// Seiten für das Rendering flatten
const posts = data?.pages.flatMap((page) => page.items) ?? []

Integration mit Error Boundaries

throwOnError von TanStack Query mit React Error Boundaries kombinieren:

tsx
// Fehler an den nächsten Error Boundary werfen
const { data } = useQuery({
  queryKey: ['critical-data'],
  queryFn: fetchCriticalData,
  throwOnError: true,
})

// Im Route- oder Component-Tree
function RouteErrorBoundary({ error }: { error: Error }) {
  return (
    <div className="error-state">
      <h2>Etwas ist schiefgelaufen</h2>
      <p>{error.message}</p>
      <button onClick={() => window.location.reload()}>Erneut versuchen</button>
    </div>
  )
}

Prefetching für bessere UX

Daten beim Hover prefetchen und Loading-States komplett eliminieren:

tsx
function PostCard({ post }: { post: Post }) {
  const queryClient = useQueryClient()

  return (
    <Link
      to="/posts/$postId"
      params={{ postId: post.id }}
      onMouseEnter={() => {
        queryClient.prefetchQuery(postQueryOptions(post.id))
      }}
    >
      {post.title}
    </Link>
  )
}

Fazit

Die wichtigsten Erkenntnisse:

  1. Query Key Factories — strukturiert eure Keys vom ersten Tag an
  2. queryOptions() — Query-Konfiguration zwischen Komponenten und Loadern teilen
  3. Bewusste Stale Times — Cache-Dauer an die Änderungsfrequenz der Daten anpassen
  4. Optimistic Updates — immer Rollback und Refetch nach Abschluss behandeln
  5. Prefetch bei Hover — der günstigste Performance-Gewinn überhaupt

Diese Patterns haben uns unzählige Stunden Debugging von stale-data- und Cache-Invalidierungs-Bugs in der Produktion erspart. Anfangen mit Key Factories und von dort aus ausbauen.