Wer 2026 mit React produktiv entwickelt, kommt an TanStack Query nicht vorbei. Die Bibliothek hat sich als de-facto-Standard fur Server State Management durchgesetzt — und v5 bringt grundlegende API-Verbesserungen: konsistentere Typen, ein einheitliches Objekt-API fur Hooks, natives Suspense-Support und deutlich bessere DevTools.
In diesem Artikel zeigen wir, wie Claude Code beim Aufbau einer vollstandigen React Query Architektur unterstutzt: von QueryClient-Konfiguration uber typsichere Query-Key-Factories bis hin zu SSR-Hydration mit Next.js App Router. Jeder Abschnitt enthalt realistische TypeScript-Beispiele, die Claude Code in ahnlicher Form vorschlagen wurde.
Was du lernst: Wie Claude Code die React Query v5 Implementierung beschleunigt — mit realistischen TypeScript-Beispielen, Best Practices und haufigen Fallstricken, die der KI-Assistent direkt erkennt und vermeidet.
React Query v5 Breaking Changes: In v5 akzeptieren alle Hooks nur noch ein einzelnes Konfigurationsobjekt (kein separater zweiter Parameter mehr). cacheTime wurde zu gcTime umbenannt. isLoading verhalt sich nun anders bei deaktivierten Queries. Claude Code kennt diese Anderungen und generiert sofort kompatiblen Code.
1. QueryClient Setup & useQuery
Die Basis jeder React Query Anwendung ist ein korrekt konfigurierter QueryClient. Claude Code generiert nicht nur den Boilerplate — es wahlt sinnvolle Defaults fur staleTime, gcTime und Retry-Verhalten basierend auf dem Projektkontext.
QueryClient mit sinnvollen Defaults
Claude Code empfiehlt eine zentrale Konfigurationsdatei, die den QueryClient mit Projekt-spezifischen Defaults initialisiert. Der entscheidende Vorteil: Man definiert das Verhalten einmal und uberschreibt es nur an den Stellen, wo es wirklich notig ist.
// lib/query-client.ts
import { QueryClient } from '@tanstack/react-query'
export function createQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
// Daten gelten 2 Minuten als frisch
staleTime: 2 * 60 * 1000,
// Cache 10 Min nach letztem Subscriber behalten
gcTime: 10 * 60 * 1000,
// 1 Retry bei Netzwerkfehlern
retry: 1,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
// Refetch nur in Produktion bei Window-Focus
refetchOnWindowFocus: process.env.NODE_ENV === 'production',
},
mutations: {
// Mutations nie automatisch wiederholen
retry: 0,
},
},
})
}
QueryClientProvider im App Root
Der QueryClientProvider muss so weit oben wie moglich im Komponentenbaum platziert werden. Claude Code erkennt automatisch, ob ein Next.js- oder Vite-Projekt vorliegt und passt die Wrapper-Struktur entsprechend an:
// app/providers.tsx (Next.js App Router)
'use client'
import { useState } from 'react'
import { QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { createQueryClient } from '@/lib/query-client'
export function Providers({ children }: { children: React.ReactNode }) {
// useState verhindert shared state zwischen Server Requests
const [queryClient] = useState(() => createQueryClient())
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}
Query-Key-Factory Pattern
Das Query-Key-Factory Pattern ist einer der wichtigsten Best Practices in TanStack Query v5. Claude Code generiert es standardmaig, wenn man nach einer skalierbaren Architektur fragt — es verhindert typisierungsfehler bei der Cache-Invalidierung:
// lib/query-keys.ts — typsichere Query-Key-Factory
export const userKeys = {
// Basis-Key fur alle User-Queries
all: ['users'] as const,
// Liste mit optionalem Filter
lists: () => [...userKeys.all, 'list'] as const,
list: (filters: UserFilters) => [...userKeys.lists(), filters] as const,
// Detail mit User-ID
details: () => [...userKeys.all, 'detail'] as const,
detail: (id: string) => [...userKeys.details(), id] as const,
}
export const postKeys = {
all: ['posts'] as const,
lists: () => [...postKeys.all, 'list'] as const,
list: (filters: PostFilters) => [...postKeys.lists(), filters] as const,
detail: (id: string) => [...postKeys.all, 'detail', id] as const,
byUser: (userId: string) => [...postKeys.all, 'byUser', userId] as const,
}
useQuery mit select-Transformation
Claude Code zeigt, wie der select-Parameter fur leistungsoptimierte Datentransformationen eingesetzt wird. Das Ergebnis wird nur re-berechnet, wenn sich die Quelldaten wirklich andern:
// hooks/use-user.ts
import { useQuery } from '@tanstack/react-query'
import { userKeys } from '@/lib/query-keys'
import { fetchUser } from '@/api/users'
interface User {
id: string
name: string
email: string
role: 'admin' | 'user' | 'viewer'
createdAt: string
}
export function useUser(userId: string) {
return useQuery({
queryKey: userKeys.detail(userId),
queryFn: () => fetchUser(userId),
// select: Daten transformieren ohne extra State
select: (data) => ({
...data,
displayName: `${data.name} (${data.role})`,
isAdmin: data.role === 'admin',
memberSince: new Date(data.createdAt).getFullYear(),
}),
// Nur laden wenn userId vorhanden
enabled: Boolean(userId),
staleTime: 5 * 60 * 1000,
})
}
// Verwendung in Komponente:
function UserProfile({ userId }: { userId: string }) {
const { data: user, isLoading, isError } = useUser(userId)
if (isLoading) return <UserSkeleton />
if (isError || !user) return <ErrorMessage />
return <div>{user.displayName} — Mitglied seit {user.memberSince}</div>
}
useQuery
Claude Code Prompt-Tipp
"Erstelle einen useQuery-Hook fur User-Profil-Daten mit select-Transformation, enabled-Guard und typsicherem Query-Key." — Claude Code generiert daraus direkt den vollstandigen Hook inklusive Interface-Definitionen und einer Skeleton-Komponente fur den Loading-State.
v5 API-Anderung: In React Query v4 war der zweite Parameter von useQuery ein Objekt mit Optionen. In v5 gibt es nur noch einen einzigen Parameter — das Konfigurationsobjekt. Claude Code generiert automatisch v5-kompatiblen Code und warnt bei v4-Mustern.
2. useMutation & Cache-Invalidierung
Mutations sind fur schreibende Operationen zustandig — API-POST, PUT und DELETE. React Query v5 macht die Cache-Invalidierung nach erfolgreichen Mutations einfacher und typsicherer als je zuvor.
Grundlegendes useMutation Setup
Claude Code erzeugt Mutation-Hooks, die konsistent Fehler behandeln und den Cache nach Erfolg aktualisieren. Das Muster mit separaten onSuccess/onError-Callbacks ist bewusstes Design fur klare Separation of Concerns:
// hooks/use-create-post.ts
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { postKeys } from '@/lib/query-keys'
import { createPost } from '@/api/posts'
interface CreatePostInput {
title: string
content: string
authorId: string
}
export function useCreatePost() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (input: CreatePostInput) => createPost(input),
onSuccess: (newPost, variables) => {
// 1. Listen invalidieren — werden automatisch neu geladen
queryClient.invalidateQueries({ queryKey: postKeys.lists() })
// 2. Den neuen Post direkt in den Cache schreiben
queryClient.setQueryData(postKeys.detail(newPost.id), newPost)
// 3. User-spezifische Liste invalidieren
queryClient.invalidateQueries({
queryKey: postKeys.byUser(variables.authorId),
})
},
onError: (error, variables, context) => {
console.error('Post-Erstellung fehlgeschlagen:', error)
// Hier: Toast-Notification, Error-Logging etc.
},
})
}
useMutation in Komponenten verwenden
Der generierte Hook integriert sich sauber in React-Komponenten. Claude Code achtet darauf, dass der Pending-State korrekt behandelt und Doppel-Submits verhindert werden:
// components/create-post-form.tsx
import { useCreatePost } from '@/hooks/use-create-post'
export function CreatePostForm({ authorId }: { authorId: string }) {
const createPost = useCreatePost()
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
const formData = new FormData(event.currentTarget)
await createPost.mutateAsync({
title: formData.get('title') as string,
content: formData.get('content') as string,
authorId,
})
}
return (
<form onSubmit={handleSubmit}>
<input name="title" placeholder="Titel" required />
<textarea name="content" placeholder="Inhalt" required />
<button
type="submit"
disabled={createPost.isPending}
>
{createPost.isPending ? 'Wird gespeichert...' : 'Veroffentlichen'}
</button>
{createPost.isError && (
<p className="error">Fehler: {createPost.error.message}</p>
)}
</form>
)
}
useMutation
setQueryData vs. invalidateQueries
Claude Code erklart den Unterschied: setQueryData schreibt sofort ohne Netzwerkrequest in den Cache (gut fur neue Objekte deren vollstandige Daten bekannt sind). invalidateQueries markiert den Cache als veraltet und lost einen Refetch aus (sicherer wenn der Server die finale Form bestimmt, z.B. durch Datenbankdefaults).
Mutation State Tracking
React Query v5 bietet fur Mutations ein vollstandiges State-Set. Claude Code nutzt diese Status-Flags systematisch um optimale UX zu gewahrleisten:
| Status-Flag |
Bedeutung |
Typischer Use Case |
isPending |
Mutation lauft gerade |
Button deaktivieren, Spinner zeigen |
isSuccess |
Mutation erfolgreich |
Success-Toast, Form zuruck setzen |
isError |
Fehler aufgetreten |
Fehlermeldung anzeigen |
isIdle |
Noch nicht gestartet |
Initialer Button-State |
data |
Erfolgs-Response |
Neue ID verwenden fur Navigation |
3. Infinite Queries & Pagination
Infinite Scrolling und cursor-basierte Pagination sind typische Use Cases fur useInfiniteQuery. Claude Code generiert den vollstandigen Stack: vom Hook bis zur IntersectionObserver-Logik fur automatisches Nachladen.
useInfiniteQuery Grundstruktur
In v5 wurde die API von useInfiniteQuery uberarbeitet. Der initialPageParam ist jetzt obligatorisch, und getNextPageParam erhalt einen strukturierteren Kontext:
// hooks/use-infinite-posts.ts
import { useInfiniteQuery } from '@tanstack/react-query'
import { postKeys } from '@/lib/query-keys'
interface PostsPage {
posts: Post[]
nextCursor: string | null
total: number
}
async function fetchPosts({ cursor, limit = 20 }: {
cursor?: string
limit?: number
}): Promise<PostsPage> {
const params = new URLSearchParams({ limit: String(limit) })
if (cursor) params.set('cursor', cursor)
const res = await fetch(`/api/posts?${params}`)
if (!res.ok) throw new Error('Posts konnten nicht geladen werden')
return res.json()
}
export function useInfinitePosts(filters: PostFilters = {}) {
return useInfiniteQuery({
queryKey: postKeys.list(filters),
queryFn: ({ pageParam }) => fetchPosts({ cursor: pageParam }),
// v5: initialPageParam ist Pflicht
initialPageParam: undefined as string | undefined,
// Cursor aus letzter Seite extrahieren
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
// Optionale Ruckwarts-Pagination
getPreviousPageParam: (firstPage) => undefined,
staleTime: 60 * 1000,
})
}
IntersectionObserver fur Auto-Load
Claude Code generiert den kompletten Infinite-Scroll-Hook mit IntersectionObserver — ohne externe Bibliotheken. Das Pattern ist zuverlassig und verhindert doppeltes Laden:
// components/infinite-post-list.tsx
import { useEffect, useRef } from 'react'
import { useInfinitePosts } from '@/hooks/use-infinite-posts'
export function InfinitePostList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
isError,
} = useInfinitePosts()
// Sentinel-Element am Ende der Liste beobachten
const sentinelRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const sentinel = sentinelRef.current
if (!sentinel) return
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
fetchNextPage()
}
},
{ rootMargin: '200px' } // 200px vor dem Ende starten
)
observer.observe(sentinel)
return () => observer.disconnect()
}, [fetchNextPage, hasNextPage, isFetchingNextPage])
if (isLoading) return <PostListSkeleton />
if (isError) return <ErrorMessage />
// pages ist ein Array von Seiten — flatten fur Rendering
const posts = data?.pages.flatMap((page) => page.posts) ?? []
return (
<div>
{posts.map((post) => (
<PostCard key={post.id} post={post} />
))}
{/* Sentinel fur IntersectionObserver */}
<div ref={sentinelRef} style={{ height: '1px' }} />
{isFetchingNextPage && <LoadingSpinner />}
{!hasNextPage && posts.length > 0 && (
<p className="end-message">Alle Beitrage geladen.</p>
)}
</div>
)
}
Performance-Tipp von Claude Code: Bei groen Listen empfiehlt Claude Code den Einsatz von @tanstack/react-virtual (TanStack Virtual) fur DOM-Virtualisierung. Die Kombination aus useInfiniteQuery und Virtual-List ist der Standard fur Listen mit tausenden von Eintrgen.
Seiten-basierte Pagination (Alternative)
Fur traditionelle Seiten-Navigation generiert Claude Code eine Variante mit page-Index statt Cursor:
// Seiten-basierte Variante mit page-Number
const infiniteQuery = useInfiniteQuery({
queryKey: ['posts', filters],
queryFn: ({ pageParam }) => fetchPostsPage(pageParam),
initialPageParam: 1,
// Nachste Seite = aktuelle + 1, wenn Daten vorhanden
getNextPageParam: (lastPage, allPages) => {
const hasMore = lastPage.posts.length === PAGE_SIZE
return hasMore ? allPages.length + 1 : undefined
},
})
// Gezieltes Laden einer bestimmten Seite
infiniteQuery.fetchNextPage()
infiniteQuery.fetchPreviousPage()
// Alle Seiten als flache Liste
const allPosts = infiniteQuery.data?.pages.flatMap(p => p.posts)
4. Optimistic Updates & Rollback
Optimistic Updates sind das Killer-Feature fur responsive UIs. Die Anderung wird sofort im UI angezeigt — und bei Fehler automatisch zuruckgerollt. Claude Code generiert das gesamte Muster inklusive Typ-sicherem Context-Objekt.
Das Optimistic Update Pattern
Das Muster besteht aus drei Phasen: Pending (sofortiges UI-Update), Error (Rollback auf alten Zustand), Settled (Cache aktualisieren). Claude Code gibt den vollstandigen Code fur alle drei Phasen aus:
// hooks/use-toggle-like.ts
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { postKeys } from '@/lib/query-keys'
interface LikeContext {
previousPost: Post | undefined
previousList: Post[] | undefined
}
export function useToggleLike(postId: string) {
const queryClient = useQueryClient()
return useMutation<
Post, // TData: Success Response
Error, // TError: Fehler-Typ
void, // TVariables: Kein Input-Parameter
LikeContext // TContext: Rollback-Daten
>({
mutationFn: () => togglePostLike(postId),
// PHASE 1: Sofortiges optimistisches Update
onMutate: async () => {
// Laufende Refetches abbrechen (verhindert Uberschreibung)
await queryClient.cancelQueries({ queryKey: postKeys.detail(postId) })
await queryClient.cancelQueries({ queryKey: postKeys.lists() })
// Snapshot fur Rollback speichern
const previousPost = queryClient.getQueryData<Post>(postKeys.detail(postId))
const previousList = queryClient.getQueryData<Post[]>(postKeys.lists())
// Detail-Cache sofort aktualisieren
queryClient.setQueryData<Post>(postKeys.detail(postId), (old) => {
if (!old) return old
return {
...old,
isLiked: !old.isLiked,
likesCount: old.isLiked ? old.likesCount - 1 : old.likesCount + 1,
}
})
// Listen-Cache sofort aktualisieren
queryClient.setQueryData<Post[]>(postKeys.lists(), (old) =>
old?.map((post) =>
post.id === postId
? { ...post, isLiked: !post.isLiked, likesCount: post.isLiked ? post.likesCount - 1 : post.likesCount + 1 }
: post
)
)
// Context zuruck geben fur Rollback im Error-Fall
return { previousPost, previousList }
},
// PHASE 2: Rollback bei Fehler
onError: (err, variables, context) => {
if (context?.previousPost) {
queryClient.setQueryData(postKeys.detail(postId), context.previousPost)
}
if (context?.previousList) {
queryClient.setQueryData(postKeys.lists(), context.previousList)
}
console.error('Like fehlgeschlagen, Rollback durchgefuhrt', err)
},
// PHASE 3: Cache mit Server-Daten synchronisieren
onSettled: () => {
queryClient.invalidateQueries({ queryKey: postKeys.detail(postId) })
},
})
}
Cache
cancelQueries ist unverzichtbar
Claude Code weist immer darauf hin: Ohne cancelQueries vor dem optimistischen Update konnte ein laufender Refetch das soeben optimistisch gesetzte Ergebnis uberschreiben. Der await ist dabei wichtig — die Cancellation muss abgeschlossen sein, bevor der Snapshot gespeichert wird.
Optimistic Update in der Praxis
// Verwendung: Like-Button mit sofortigem Feedback
function LikeButton({ postId }: { postId: string }) {
const { data: post } = useQuery({
queryKey: postKeys.detail(postId),
queryFn: () => fetchPost(postId),
})
const toggleLike = useToggleLike(postId)
return (
<button
onClick={() => toggleLike.mutate()}
disabled={toggleLike.isPending}
aria-label={post?.isLiked ? 'Like entfernen' : 'Liken'}
>
{post?.isLiked ? 'Geliked' : 'Liken'} ({post?.likesCount})
</button>
)
}
Praxis-Erfahrung: Bei Netzwerklatenz uber 100ms ist der Unterschied mit Optimistic Updates deutlich spurbar. Claude Code empfiehlt, Optimistic Updates fur alle haufig genutzten, idempotenten Aktionen einzusetzen: Likes, Bookmarks, Toggles, Reorder-Aktionen.
5. Suspense & Error Boundaries
React Query v5 bringt natives Suspense-Support mit useSuspenseQuery. Claude Code kombiniert es mit React Error Boundaries fur deklaratives Async-Rendering ohne isLoading/isError Checks in jeder Komponente.
useSuspenseQuery statt useQuery
Der entscheidende Unterschied: useSuspenseQuery wirft Promises bei Loading und Errors direkt — dadurch entfallen die ublichen Conditional-Checks und der Typ von data ist immer definiert (kein undefined):
// hooks/use-user-suspense.ts
import { useSuspenseQuery } from '@tanstack/react-query'
import { userKeys } from '@/lib/query-keys'
export function useUserSuspense(userId: string) {
return useSuspenseQuery({
queryKey: userKeys.detail(userId),
queryFn: () => fetchUser(userId),
staleTime: 5 * 60 * 1000,
})
}
// Komponente: Kein isLoading/isError notig!
function UserDetail({ userId }: { userId: string }) {
// data ist immer User (niemals undefined) dank Suspense
const { data: user } = useUserSuspense(userId)
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
)
}
Suspense Boundaries im Layout
Claude Code strukturiert die Suspense-Grenzen so, dass Skeleton-UIs gezielt fur einzelne Seitenbereiche angezeigt werden. Die Error Boundary fangt API-Fehler ohne manuelles isError-Handling:
// app/dashboard/page.tsx (Next.js App Router)
import { Suspense } from 'react'
import { ErrorBoundary } from 'react-error-boundary'
export default function DashboardPage() {
return (
<main>
<h1>Dashboard</h1>
{/* Unabhangige Suspense-Grenzen fur paralleles Laden */}
<div className="dashboard-grid">
<ErrorBoundary fallback={<WidgetError title="Benutzer" />}>
<Suspense fallback={<UserWidgetSkeleton />}>
<UserWidget />
</Suspense>
</ErrorBoundary>
<ErrorBoundary fallback={<WidgetError title="Statistiken" />}>
<Suspense fallback={<StatsSkeleton />}>
<StatsWidget />
</Suspense>
</ErrorBoundary>
<ErrorBoundary fallback={<WidgetError title="Aktivitat" />}>
<Suspense fallback={<ActivitySkeleton />}>
<ActivityFeed />
</Suspense>
</ErrorBoundary>
</div>
</main>
)
}
Error Boundary mit Reset
Claude Code integriert react-error-boundary mit React Query Reset-Funktionalitat, damit Nutzer nach einem Fehler die Daten neu laden konnen:
// components/query-error-boundary.tsx
import { ErrorBoundary, FallbackProps } from 'react-error-boundary'
import { useQueryErrorResetBoundary } from '@tanstack/react-query'
function QueryErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div role="alert" className="error-fallback">
<h3>Etwas ist schief gelaufen</h3>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>
Erneut versuchen
</button>
</div>
)
}
export function QueryErrorBoundary({ children }: { children: React.ReactNode }) {
const { reset } = useQueryErrorResetBoundary()
return (
<ErrorBoundary
onReset={reset}
FallbackComponent={QueryErrorFallback}
>
{children}
</ErrorBoundary>
)
}
Suspense
useSuspenseQueries fur parallele Queries
Claude Code empfiehlt useSuspenseQueries wenn mehrere Queries gleichzeitig geladen werden mussen. Alle Promises werden parallel aufgeloest — erst wenn alle fertig sind, wird die Komponente gerendert. Das verhindert die "Wasserfall"-Problematik bei sequentiellen useQuery-Aufrufen.
6. Prefetching & SSR-Integration
Prefetching und SSR-Hydration sind entscheidend fur Core Web Vitals. Claude Code generiert den kompletten Dehydrate/Hydrate-Workflow fur Next.js App Router — inkl. HydrationBoundary und Server-Component-Integration.
Prefetching in Server Components
Mit Next.js App Router konnen Queries direkt in Server Components prefetched werden. Die Daten werden dehydriert, zum Client gesendet und dort in den QueryClient hydratisiert — ohne doppelten Netzwerkrequest:
// app/posts/page.tsx — Server Component
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'
import { postKeys } from '@/lib/query-keys'
import { PostList } from '@/components/post-list'
import { fetchPosts } from '@/api/posts'
export default async function PostsPage() {
// Neuer QueryClient fur jeden Request (wichtig bei SSR!)
const queryClient = new QueryClient()
// Posts auf dem Server prefetchen
await queryClient.prefetchQuery({
queryKey: postKeys.list({}),
queryFn: () => fetchPosts({}),
staleTime: 60 * 1000,
})
// Mehrere Queries parallel prefetchen
await Promise.all([
queryClient.prefetchQuery({
queryKey: postKeys.list({ category: 'featured' }),
queryFn: () => fetchPosts({ category: 'featured' }),
}),
queryClient.prefetchQuery({
queryKey: ['categories'],
queryFn: fetchCategories,
}),
])
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<PostList />
</HydrationBoundary>
)
}
Client Component mit hydrierten Daten
Die Client Component nutzt useQuery wie gewohnt. React Query erkennt automatisch, dass der Cache bereits befullte Daten enthalt und macht keinen erneuten Netzwerkrequest:
// components/post-list.tsx — Client Component
'use client'
import { useQuery } from '@tanstack/react-query'
import { postKeys } from '@/lib/query-keys'
export function PostList() {
// Kein Loading-State: Daten sind bereits vom Server vorhanden
const { data: posts } = useQuery({
queryKey: postKeys.list({}),
queryFn: () => fetchPosts({}),
// staleTime verhindert sofortigen Refetch nach Hydration
staleTime: 60 * 1000,
})
return (
<div className="post-grid">
{posts?.map((post) => (
<PostCard key={post.id} post={post} />
))}
</div>
)
}
prefetchQuery vs. prefetchInfiniteQuery
// Infinite Query auch auf dem Server prefetchen
await queryClient.prefetchInfiniteQuery({
queryKey: postKeys.list({ type: 'infinite' }),
queryFn: ({ pageParam }) => fetchPosts({ cursor: pageParam }),
initialPageParam: undefined,
// Nur die erste Seite prefetchen — Rest per Infinite Scroll
pages: 1,
})
Streaming mit Suspense (Next.js App Router)
Claude Code kombiniert React Query Prefetching mit Next.js Streaming fur progressive Seitenauslieferung. Kritische Daten werden synchron gerendert, weniger wichtige Inhalte werden gestreamt:
// app/dashboard/page.tsx — Streaming Setup
import { Suspense } from 'react'
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'
export default async function DashboardPage() {
const queryClient = new QueryClient()
// Kritische Daten: await (blockiert bis bereit)
await queryClient.prefetchQuery({
queryKey: ['user', 'me'],
queryFn: fetchCurrentUser,
})
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<UserHeader /> {/* Sofort sichtbar */}
<Suspense fallback={<StatsSkeleton />}>
{/* Gestreamt wenn bereit */}
<AsyncStatsSection queryClient={queryClient} />
</Suspense>
<Suspense fallback={<FeedSkeleton />}>
<AsyncActivityFeed queryClient={queryClient} />
</Suspense>
</HydrationBoundary>
)
}
SSR
Wichtig: Pro-Request QueryClient
Claude Code betont immer: In SSR-Kontexten muss new QueryClient() fur jeden einzelnen Request aufgerufen werden — niemals ein globaler Singleton auf dem Server. Andernfalls teilen sich verschiedene Nutzer denselben Cache, was zu Datenlecks fuhrt. Der Singleton-Pattern ist nur auf dem Client sicher (via useState).
| Methode |
Zeitpunkt |
Netzwerkrequest Client |
Best fur |
prefetchQuery (Server) |
Vor erstem Render |
Kein Request (Cache-Hit) |
Kritische Seitendaten, SEO |
prefetchQuery (Client) |
Hover/Navigation |
Vorausgeladener Cache |
Antizipierte Navigation |
useQuery ohne Prefetch |
Beim Mounten |
Immer Request |
Seltene/nutzerspezifische Daten |
initialData |
Synchron |
Nur nach staleTime |
Statische/seltene Daten |
Fazit: React Query v5 mit Claude Code
TanStack Query v5 ist 2026 die klarste Wahl fur Server State Management in React-Projekten. Die API ist konsistenter als je zuvor, TypeScript-Support ist erstklassig und die Kombination aus Suspense, SSR-Hydration und DevTools macht die Entwicklung signifikant produktiver.
Claude Code beschleunigt die Implementierung auf mehreren Ebenen:
- Architektur-Entscheidungen: Query-Key-Factory, Hook-Struktur, Suspense-Grenzen — Claude Code schlagt direkt skalierbare Muster vor
- Boilerplate: QueryClient-Setup, Provider-Wrapping, Mutation-Hooks mit vollstandigem Error-Handling entstehen in Sekunden
- v5-Kompatibilitat: Claude Code kennt alle Breaking Changes und generiert direkt korrekten v5-Code — kein manuelles Durchsuchen von Changelogs
- Edge Cases: cancelQueries vor Optimistic Updates, Pro-Request QueryClient fur SSR, staleTime-Abstimmung — Claude Code erklart und implementiert korrekt
- Typsicherheit: Generics fur Mutation-Context, select-Transformationen, useSuspenseQuery ohne undefined — vollstandige TypeScript-Integration
Was Claude Code nicht ersetzt: Das Verstandnis fur wann welches Pattern sinnvoll ist — Optimistic Updates sind nicht fur jede Mutation geeignet, Suspense erfordert durchdachte Error-Boundaries, SSR-Prefetching macht nur Sinn fur tatsachlich SEO-kritische Inhalte. Das architektonische Urteil bleibt beim Entwickler.
Die sechs vorgestellten Muster — QueryClient-Setup, Mutations, Infinite Queries, Optimistic Updates, Suspense und SSR-Integration — decken den Groten Teil realer React-Projekte ab. Mit Claude Code als Implementierungsassistent lasst sich der initiale Aufbau dieser Architektur von Tagen auf Stunden reduzieren.
State-Management-Modul im Kurs
Im Claude Code Mastery Kurs: vollstandiges TanStack Query-Modul mit Optimistic Updates, Infinite Queries, Suspense und SSR-Integration fur Next.js. Schritt-fur-Schritt mit echten Projekten.
14 Tage kostenlos testen →