TanStack Query Advanced Patterns mit Claude Code 2026

TanStack Query v5 ist mehr als ein Datenfetch-Tool -- Optimistic Updates, Infinite Queries, SSR-Prefetching und intelligentes Cache-Management machen es zur vollstaendigen Server-State-Loesung. Claude Code kennt alle Advanced Patterns fuer produktionsreife React-Anwendungen.

1. Optimistic Updates — onMutate / onError / onSettled

Optimistic Updates sind der Unterschied zwischen einer App, die sich traege anfuehlt, und einer die sofort reagiert. Statt auf den Server zu warten, aktualisierst du den lokalen Cache sofort — und rollst zurueck, falls der Server einen Fehler zurueckgibt. TanStack Query v5 hat dafuer das onMutate / onError / onSettled-Muster eingebaut.

OPTIMISTIC Das vollstaendige useMutation-Rollback-Pattern

Das Muster besteht aus drei Phasen: (1) Snapshot vor der Mutation, (2) sofortige Cache-Aktualisierung, (3) Rollback bei Fehler. Claude Code generiert dieses Boilerplate auf Knopfdruck.

import { useMutation, useQueryClient } from '@tanstack/react-query' import { updateTodo } from '../api/todos' interface Todo { id: string title: string completed: boolean } export function useUpdateTodo() { const queryClient = useQueryClient() return useMutation({ mutationFn: (todo: Todo) => updateTodo(todo), // Phase 1: Snapshot + sofortige Aktualisierung async onMutate(newTodo) { // Laufende Refetches abbrechen (keine Race Conditions) await queryClient.cancelQueries({ queryKey: ['todos'] }) // Snapshot des aktuellen Cache-Zustands sichern const previousTodos = queryClient.getQueryData<Todo[]>(['todos']) // Cache sofort optimistisch aktualisieren queryClient.setQueryData<Todo[]>(['todos'], (old) => old?.map((todo) => todo.id === newTodo.id ? { ...todo, ...newTodo } : todo ) ?? [] ) // Snapshot fuer Rollback zurueckgeben return { previousTodos } }, // Phase 2: Rollback bei Fehler onError(err, newTodo, context) { if (context?.previousTodos) { queryClient.setQueryData(['todos'], context.previousTodos) } console.error('Mutation fehlgeschlagen, Rollback durchgefuehrt:', err) }, // Phase 3: Immer neu synchronisieren (Erfolg ODER Fehler) onSettled() { queryClient.invalidateQueries({ queryKey: ['todos'] }) }, }) }

OPTIMISTIC Optimistic Update im Komponent verwenden

Der Hook haelt die API sauber — die Komponente merkt nichts von der Komplexitaet dahinter. Das ist Clean Architecture fuer Data Fetching.

function TodoItem({ todo }: { todo: Todo }) { const { mutate: updateTodo, isPending } = useUpdateTodo() return ( <li style={{ opacity: isPending ? 0.6 : 1 }}> <input type="checkbox" checked={todo.completed} onChange={(e) => updateTodo({ ...todo, completed: e.target.checked }) } /> <span>{todo.title}</span> {isPending && <Spinner size="sm" />} </li> ) }
Wichtig: cancelQueries in onMutate ist unverzichtbar. Ohne diesen Aufruf kann ein paralleler Refetch den optimistischen Update ueberschreiben — Race Condition garantiert. Claude Code erinnert dich automatisch daran.
Claude Code Tipp: Frage claude "Optimistic Update mit TanStack Query v5 und TypeScript — vollstaendiges Rollback-Pattern mit cancelQueries" — du bekommst sofort typsicheres Boilerplate fuer deinen spezifischen Anwendungsfall.

2. Infinite Queries — useInfiniteQuery mit Auto-Load

Paginated APIs sind der Standard — aber "Lade mehr"-Buttons fuehlen sich 2026 veraltet an. Mit useInfiniteQuery und IntersectionObserver baust du echtes Auto-Loading, das Seiten automatisch nachlaedt, sobald der Nutzer den unteren Rand erreicht.

INFINITE useInfiniteQuery mit getNextPageParam

TanStack Query v5 hat die useInfiniteQuery-API vereinfacht: initialPageParam ersetzt das alte defaultPageParam, und getNextPageParam bekommt den letzten Page-Response als ersten Parameter.

import { useInfiniteQuery } from '@tanstack/react-query' interface PostsPage { posts: Post[] nextCursor: string | null totalCount: number } export function useInfinitePosts(filter?: string) { return useInfiniteQuery({ queryKey: ['posts', 'infinite', { filter }], queryFn: ({ pageParam }) => fetchPosts({ cursor: pageParam, filter, limit: 20 }), // v5: initialPageParam statt defaultPageParam initialPageParam: null as string | null, // Gibt undefined zurueck wenn keine weitere Seite existiert getNextPageParam: (lastPage: PostsPage) => lastPage.nextCursor ?? undefined, // Optional: Bidirektionales Paginieren getPreviousPageParam: (firstPage: PostsPage) => firstPage.nextCursor ?? undefined, // Nur 3 Seiten gleichzeitig im Cache halten maxPages: 3, }) } // Hilfsfunktion: alle Posts aus allen Seiten flach mappen export function flattenPages<T>( pages: Array<{ posts: T[] }> | undefined ): T[] { return pages?.flatMap((page) => page.posts) ?? [] }

INFINITE IntersectionObserver fuer automatisches Nachladen

Kein "Lade mehr"-Button noetig. Ein IntersectionObserver auf einem Sentinel-Element am Ende der Liste loest automatisch fetchNextPage aus.

import { useEffect, useRef } from 'react' import { useInfinitePosts, flattenPages } from './useInfinitePosts' export function PostsFeed({ filter }: { filter?: string }) { const { data, fetchNextPage, hasNextPage, isFetchingNextPage, status, } = useInfinitePosts(filter) // Sentinel-Ref fuer IntersectionObserver const loadMoreRef = useRef<HTMLDivElement>(null) useEffect(() => { const sentinel = loadMoreRef.current if (!sentinel) return const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) { fetchNextPage() } }, { rootMargin: '200px' } ) observer.observe(sentinel) return () => observer.disconnect() }, [fetchNextPage, hasNextPage, isFetchingNextPage]) const posts = flattenPages(data?.pages) if (status === 'pending') return <PostsSkeleton /> if (status === 'error') return <ErrorMessage /> return ( <> <ul> {posts.map((post) => ( <PostCard key={post.id} post={post} /> ))} </ul> <div ref={loadMoreRef} style={{ height: '1px' }}> {isFetchingNextPage && <LoadingSpinner />} {!hasNextPage && <p>Alle Posts geladen.</p>} </div> </> ) }
Performance-Tipp: Mit maxPages: 3 haelt TanStack Query nur 3 Seiten gleichzeitig im Speicher. Aeltere Seiten werden automatisch verworfen — ideal fuer sehr lange Listen, die sonst zu Speicherproblemen fuehren wuerden.

3. Query Prefetching — SSR mit HydrationBoundary

Server-Side Rendering mit TanStack Query bedeutet: Daten auf dem Server laden, in den Client-Cache hydratisieren und dem Nutzer sofort befuellte Seiten zeigen — ohne Skeleton-Loader. Next.js und Remix werden von TanStack Query v5 nativ unterstuetzt.

SSR prefetchQuery im Next.js App Router Loader

Im App Router laedt du Daten direkt in der Server Component via prefetchQuery. Der dehydrate-Aufruf serialisiert den Cache fuer den Transport zum Client.

// app/posts/page.tsx -- Server Component import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query' import { getPosts } from '@/lib/api' import { PostsList } from './PostsList' export default async function PostsPage() { const queryClient = new QueryClient() // Daten auf dem Server prefetchen await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, staleTime: 60 * 1000, }) // Optional: mehrere Queries parallel prefetchen await Promise.all([ queryClient.prefetchQuery({ queryKey: ['categories'], queryFn: getCategories }), queryClient.prefetchQuery({ queryKey: ['featured'], queryFn: getFeatured }), ]) return ( <HydrationBoundary state={dehydrate(queryClient)}> <PostsList /> </HydrationBoundary> ) }

CLIENT Client Component nutzt Server-Daten sofort

Die Client Component sieht denselben useQuery-Aufruf wie immer — nur dass die Daten bereits im Cache sind. Kein Skeleton, kein Flackern.

'use client' import { useQuery } from '@tanstack/react-query' import { getPosts } from '@/lib/api' export function PostsList() { // Kein isPending-Check noetig -- Daten kommen vom Server const { data: posts, isStale } = useQuery({ queryKey: ['posts'], queryFn: getPosts, staleTime: 60 * 1000, }) return ( <div> {isStale && <RefreshIndicator />} {posts?.map((post) => ( <PostCard key={post.id} post={post} /> ))} </div> ) }

ROUTER Hover-basiertes Prefetching vor Navigation

Naechste Seite schon laden, bevor der Nutzer klickt — ein klassischer Performance-Trick. Mit TanStack Query und Next.js Link geht das elegant.

'use client' import { useQueryClient } from '@tanstack/react-query' import Link from 'next/link' function PostLink({ postId, children }: { postId: string; children: React.ReactNode }) { const queryClient = useQueryClient() const handleMouseEnter = () => { // Post-Detail beim Hover schon laden queryClient.prefetchQuery({ queryKey: ['post', postId], queryFn: () => fetchPost(postId), staleTime: 30 * 1000, }) } return ( <Link href={`/posts/${postId}`} onMouseEnter={handleMouseEnter}> {children} </Link> ) }

4. Custom Query Hooks — Query Key Factories & Typed Selectors

Ad-hoc Query Keys als Strings zu schreiben fuehrt zu Inkonsistenzen und schwer zu debuggenden Cache-Invalidierungsproblemen. Query Key Factories sind das Muster, das 2026 in jedem serioesenReact-Projekt zu finden ist — und Claude Code generiert sie automatisch.

FACTORY Query Key Factory — typsicher und wartbar

Ein zentrales Key-Objekt stellt sicher, dass alle Cache-Zugriffe konsistente Keys verwenden. Invalidierungen auf Root-Ebene funktionieren automatisch fuer alle Sub-Keys.

// lib/queryKeys.ts -- Zentrale Key Factory export const postKeys = { // Root: invalidiert ALLES unter 'posts' all: ['posts'] as const, // Listen mit optionalem Filter lists: () => [...postKeys.all, 'list'] as const, list: (filter: PostFilter) => [...postKeys.lists(), { filter }] as const, // Einzelne Posts details: () => [...postKeys.all, 'detail'] as const, detail: (id: string) => [...postKeys.details(), id] as const, // Post-Kommentare comments: (postId: string) => [...postKeys.detail(postId), 'comments'] as const, } as const // Verwendung: // postKeys.all -> ['posts'] // postKeys.list({}) -> ['posts', 'list', {}] // postKeys.detail('1') -> ['posts', 'detail', '1'] // postKeys.comments('1')-> ['posts', 'detail', '1', 'comments']

SELECTOR Typed Selectors mit der select-Option

Die select-Option transformiert Query-Daten direkt in der Query — kein Extra-useMemo noetig. Der Selector wird gecacht und nur neu berechnet wenn sich die Quelldaten aendern.

import { useQuery } from '@tanstack/react-query' import { postKeys } from '@/lib/queryKeys' // Basis-Hook -- gibt vollstaendige Post-Daten zurueck export function usePost(id: string) { return useQuery({ queryKey: postKeys.detail(id), queryFn: () => fetchPost(id), staleTime: 5 * 60 * 1000, }) } // Spezialisierter Hook -- nur Metadaten extrahieren export function usePostTitle(id: string) { return useQuery({ queryKey: postKeys.detail(id), queryFn: () => fetchPost(id), // Selector: nur title + author extrahieren select: (post) => ({ title: post.title, authorName: post.author.name, publishedAt: new Date(post.publishedAt), }), }) } // data ist jetzt { title: string; authorName: string; publishedAt: Date } // Re-render nur wenn title/author/date sich aendern // Selector-Hook fuer Listen -- zaehlt nur export function usePostCount(filter: PostFilter) { return useQuery({ queryKey: postKeys.list(filter), queryFn: () => fetchPosts(filter), select: (data) => data.totalCount, }) }
Claude Code Tipp: Query Key Factories und typisierte Selectors werden von Claude Code als Pair generiert — frage einfach claude "Erstelle Query Key Factory + typed useQuery hooks fuer meine Posts-API mit diesen Feldern: [deine Felder]".

5. Dependent Queries — enabled, Chaining, Error Propagation

In der Realitaet haengen Queries oft voneinander ab: Erst User laden, dann Posts des Users, dann Kommentare zum ersten Post. TanStack Query loest das mit der enabled-Option — elegant, typsicher, ohne Callback-Hoelle.

ENABLED Query Chaining mit enabled-Option

Die enabled-Option akzeptiert einen Boolean. Ist er false, bleibt die Query im pending-Zustand und startet keinen Fetch — perfekt fuer Abhaengigkeitsketten.

import { useQuery } from '@tanstack/react-query' function UserPostsWithComments({ userId }: { userId: string }) { // Query 1: User-Daten laden const userQuery = useQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId), }) // Query 2: Posts laden -- NUR wenn User geladen ist const postsQuery = useQuery({ queryKey: ['posts', 'user', userId], queryFn: () => fetchUserPosts(userId), // enabled: false solange userQuery laeuft oder Fehler hat enabled: userQuery.isSuccess, }) // Query 3: Kommentare -- NUR wenn Posts existieren const firstPostId = postsQuery.data?.[0]?.id const commentsQuery = useQuery({ queryKey: ['comments', firstPostId], queryFn: () => fetchComments(firstPostId!), // Beide Bedingungen: Posts erfolgreich UND firstPostId existiert enabled: postsQuery.isSuccess && !!firstPostId, }) return ( <div> <UserCard user={userQuery.data} isPending={userQuery.isPending} /> <PostsList posts={postsQuery.data} isPending={postsQuery.isPending} /> <CommentsList comments={commentsQuery.data} /> </div> ) }

ERROR Error Propagation in abhaengigen Query-Ketten

Was passiert wenn Query 1 scheitert? Query 2 und 3 bleiben im pending-Zustand — kein Fetch, kein Fehler, aber auch kein Fortschritt. Das musst du explizit behandeln.

function UserDashboard({ userId }: { userId: string }) { const userQuery = useQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId), retry: 2, }) const postsQuery = useQuery({ queryKey: ['posts', userId], queryFn: () => fetchUserPosts(userId), enabled: userQuery.isSuccess, }) // Explizite Error-Behandlung fuer die ganze Kette if (userQuery.isError) { return <ErrorBoundaryFallback error={userQuery.error} retry={userQuery.refetch} /> } if (userQuery.isPending) { return <DashboardSkeleton /> } return ( <div> <h1>{userQuery.data.name}s Dashboard</h1> {postsQuery.isPending && <PostsSkeleton />} {postsQuery.isError && <PostsError error={postsQuery.error} />} {postsQuery.data && <PostsList posts={postsQuery.data} />} </div> ) }

ADVANCED useQueries fuer dynamische Query-Arrays

Wenn du eine Liste von IDs hast und fuer jede ID eine Query starten willst, ist useQueries die richtige Wahl — kein Hook in einer Schleife, sondern ein typsicherer Array-Query.

import { useQueries } from '@tanstack/react-query' function MultiUserDisplay({ userIds }: { userIds: string[] }) { const userQueries = useQueries({ queries: userIds.map((id) => ({ queryKey: ['user', id], queryFn: () => fetchUser(id), staleTime: 5 * 60 * 1000, })), // Optional: gemeinsame Transformation aller Ergebnisse combine: (results) => ({ users: results.map((r) => r.data).filter(Boolean), isLoading: results.some((r) => r.isPending), hasErrors: results.some((r) => r.isError), }), }) if (userQueries.isLoading) return <Spinner /> return ( <ul> {userQueries.users.map((user) => ( <UserCard key={user.id} user={user} /> ))} </ul> ) }

6. Cache Manipulation — setQueryData, invalidateQueries, cancelQueries

Der TanStack Query Cache ist kein Black Box — du hast vollstaendige Kontrolle. setQueryData, invalidateQueries und cancelQueries sind die drei Werkzeuge fuer praezises Cache-Management. Claude Code kennt alle Edge Cases.

CACHE setQueryData — direktes Cache-Schreiben

Nach einer erfolgreichen Mutation kannst du den Cache direkt aktualisieren, statt einen Refetch auszuloesen. Das ist schneller und spart Netzwerk-Requests.

import { useMutation, useQueryClient } from '@tanstack/react-query' import { postKeys } from '@/lib/queryKeys' export function useCreatePost() { const queryClient = useQueryClient() return useMutation({ mutationFn: (newPost: CreatePostInput) => createPost(newPost), onSuccess(createdPost) { // 1. Neuen Post direkt in den Detail-Cache schreiben queryClient.setQueryData( postKeys.detail(createdPost.id), createdPost ) // 2. Post in alle Listen-Caches vorne einfuegen queryClient.setQueriesData<Post[]>( { queryKey: postKeys.lists() }, (oldPosts) => [createdPost, ...(oldPosts ?? [])] ) // 3. Restliche Queries invalidieren (Paginierung etc.) queryClient.invalidateQueries({ queryKey: postKeys.all, predicate: (query) => !query.queryKey.includes('detail'), }) }, }) }

INVALIDATE invalidateQueries — praezise & granular

Nicht immer willst du alles invalidieren. Mit refetchType, exact und predicate steuerst du genau, welche Queries neu geladen werden.

const queryClient = useQueryClient() // Alles unter 'posts' invalidieren queryClient.invalidateQueries({ queryKey: ['posts'] }) // Nur Listen invalidieren, Details in Ruhe lassen queryClient.invalidateQueries({ queryKey: postKeys.lists() }) // Genau diese eine Query invalidieren (exact match) queryClient.invalidateQueries({ queryKey: postKeys.list({ category: 'tech' }), exact: true, }) // Nur aktive Queries refetchen, inaktive nur markieren queryClient.invalidateQueries({ queryKey: ['posts'], refetchType: 'active', // 'all' | 'active' | 'inactive' | 'none' }) // Predicate: nur veraeltete Listen invalidieren queryClient.invalidateQueries({ predicate: (query) => { return ( query.queryKey[0] === 'posts' && query.state.dataUpdatedAt < Date.now() - 5 * 60 * 1000 ) }, })

CANCEL cancelQueries — Race Conditions verhindern

cancelQueries ist nicht nur fuer Optimistic Updates wichtig. Auch beim Unmount einer Komponente oder beim Navigation-Abbruch willst du laufende Fetches stoppen.

import { useEffect } from 'react' import { useQueryClient } from '@tanstack/react-query' // Automatisches Abbrechen beim Unmount function HeavyDataComponent({ reportId }: { reportId: string }) { const queryClient = useQueryClient() useEffect(() => { return () => { // Beim Unmount: laufende Report-Queries abbrechen queryClient.cancelQueries({ queryKey: ['report', reportId] }) } }, [queryClient, reportId]) return <ReportView reportId={reportId} /> } // Mit AbortSignal in queryFn integrieren const query = useQuery({ queryKey: ['large-report', reportId], queryFn: ({ signal }) => fetch(`/api/reports/${reportId}`, { signal }) .then((r) => r.json()), // TanStack Query uebergibt AbortSignal automatisch // cancelQueries() ruft signal.abort() auf }) // removeQueries -- Cache-Eintraege komplett loeschen queryClient.removeQueries({ queryKey: ['user', userId] }) // resetQueries -- zurueck auf initialData setzen queryClient.resetQueries({ queryKey: ['posts'] })

DEVTOOLS Cache-Zustand debuggen mit TanStack Query Devtools

Die offiziellen Devtools zeigen dir den kompletten Cache in Echtzeit — Query Keys, Status, Stale-Time, Observer-Anzahl. Unverzichtbar fuer komplexe Cache-Strategien.

// app/providers.tsx 'use client' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' import { useState } from 'react' export function Providers({ children }: { children: React.ReactNode }) { const [queryClient] = useState( () => new QueryClient({ defaultOptions: { queries: { staleTime: 60 * 1000, throwOnError: (error) => error instanceof UnauthorizedError, }, }, }) ) return ( <QueryClientProvider client={queryClient}> {children} <ReactQueryDevtools initialIsOpen={false} /> </QueryClientProvider> ) }
Cache-Strategie Empfehlung: Verwende nie invalidateQueries({ queryKey: [] }) ohne weiteren Scope — das invalidiert deinen kompletten Cache und loest massenhaft Netzwerk-Requests aus. Immer so spezifisch wie moeglich sein.
Zusammenfassung der Cache-Operationen:
  • setQueryData — Cache direkt schreiben (keine Netzwerkkosten)
  • invalidateQueries — Als veraltet markieren + optional refetchen
  • cancelQueries — Laufende Fetches abbrechen (Race Conditions)
  • removeQueries — Cache-Eintraege komplett loeschen
  • resetQueries — Auf initialData zuruecksetzen

Fazit: TanStack Query v5 + Claude Code = produktionsreifes Data Fetching

TanStack Query v5 hat in 2026 endgueltig den Status als Standard-Loesung fuer Server State in React-Anwendungen erreicht. Die sechs Advanced Patterns in diesem Artikel — Optimistic Updates, Infinite Queries, SSR-Prefetching, Custom Query Hooks, Dependent Queries und Cache Manipulation — bilden zusammen eine vollstaendige Architektur fuer anspruchsvolle Web-Applikationen.

Claude Code kennt alle diese Patterns in- und auswendig. Es generiert nicht nur Boilerplate, sondern versteht die Semantik: Wann ist ein Optimistic Update sinnvoll? Wo brauche ich cancelQueries? Wie baue ich eine Query Key Factory fuer mein spezifisches API-Design? Diese Fragen beantwortet Claude Code mit konkretem, typsicherem TypeScript-Code — angepasst an dein Projekt.

Der naechste Schritt: Richte TanStack Query in deinem Next.js-Projekt ein und lass Claude Code deine bestehenden fetch-Calls migrieren. Du wirst überrascht sein, wie viel Boilerplate wegfaellt und wie viel klarer die Fehlerbehandlung wird.

CHECKLISTE TanStack Query v5 Advanced Patterns — Quick Reference

  • Optimistic Updates: onMutate + Snapshot + cancelQueries + onError Rollback + onSettled invalidate
  • Infinite Queries: useInfiniteQuery + initialPageParam + getNextPageParam + IntersectionObserver
  • SSR Prefetching: prefetchQuery in Server Component + dehydrate + HydrationBoundary
  • Custom Hooks: Query Key Factory (as const) + typisierte select-Selectors
  • Dependent Queries: enabled: otherQuery.isSuccess + explizite Error-Behandlung
  • Cache Manipulation: setQueryData nach Mutations + granulare invalidateQueries mit predicate

Data-Fetching-Modul im Kurs

Im Claude Code Mastery Kurs: TanStack Query von Grundlagen bis Advanced — Optimistic Updates, SSR, Custom Hooks und Performance-Optimierung mit echten Projekten und Code-Reviews.

14 Tage kostenlos testen →