Teil unserer Performance & Scalability-Serie
Den vollständigen Leitfaden lesenReact Query (TanStack): Datenabrufmuster
Das Abrufen von Daten in React hat eine überraschend große Oberfläche: Ladezustände, Fehlerzustände, erneutes Abrufen im Hintergrund, Cache-Invalidierung nach Mutationen, optimistische Aktualisierungen, unendliches Scrollen, Vorabrufen und Deduplizierung. Das manuelle Schreiben dieser Logik mit useEffect + useState führt zu Race Conditions, Speicherverlusten und inkonsistenten UI-Zuständen. TanStack Query (ehemals React Query) v5 löst all dieses Problem mit einer sauberen, zusammensetzbaren API.
In diesem Leitfaden werden TanStack Query v5-Muster für Produktions-React-Anwendungen behandelt – von einfachen Abfragen bis hin zu komplexen Mutationsflüssen, optimistischen Updates, unendlichem Scrollen und der Next.js App Router-Integration. Jedes Muster umfasst TypeScript und Fehlerbehandlung.
Wichtige Erkenntnisse
– TanStack Query ist eine Server-Statusbibliothek – verwenden Sie sie nicht für den reinen Client-UI-Status (verwenden Sie dazu „Zustand“ oder „useState“). – Abfrageschlüssel sind die Cache-Adresse – umfassen alle Variablen, die sich auf das Abfrageergebnis auswirken –
staleTimesteuert, wann das erneute Abrufen im Hintergrund ausgelöst wird;gcTimesteuert, wann nicht verwendete Daten durch Müll gesammelt werden – Mutationen sollten verwandte Abfragen überqueryClient.invalidateQueries()inonSuccessungültig machen. – Optimistische Updates verbessern die wahrgenommene Leistung, erfordern jedoch eine Rollback-Logik inonError– Verwenden SieuseInfiniteQueryfür paginierte Listen; Kombinieren Sie niemalsuseQuery+ Handbuchseitenstatus
- Prefetch bei Hover für sofortiges Navigationsgefühl:
queryClient.prefetchQuery()– Verwenden Sie im Next.js App RouterHydrationBoundary, um vom Server abgerufene Daten in den Client zu übertragen
Einrichtung und Konfiguration
pnpm add @tanstack/react-query @tanstack/react-query-devtools
// src/providers/query-provider.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { useState } from 'react';
export function QueryProvider({ children }: { children: React.ReactNode }) {
// Create QueryClient inside component to avoid shared state between requests (SSR safety)
const [queryClient] = useState(() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute — data is fresh for 1 minute
gcTime: 5 * 60 * 1000, // 5 minutes — keep unused cache for 5 minutes
retry: 2, // Retry failed requests twice
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30_000), // Exponential backoff
refetchOnWindowFocus: true, // Refetch when user returns to tab
refetchOnReconnect: true, // Refetch after network reconnect
},
mutations: {
retry: 0, // Do not retry mutations (non-idempotent)
},
},
})
);
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
// src/app/[locale]/layout.tsx
import { QueryProvider } from '@/providers/query-provider';
export default function LocaleLayout({ children }: { children: React.ReactNode }) {
return (
<QueryProvider>
{children}
</QueryProvider>
);
}
Abfrageschlüssel – Die Stiftung
Abfrageschlüssel sind die Cache-Adresse für Ihre Daten. Jede Variable, die das Abfrageergebnis beeinflusst, muss im Schlüssel enthalten sein.
// src/lib/query-keys.ts
// Centralize query keys to avoid typos and enable easy invalidation
export const queryKeys = {
contacts: {
all: () => ['contacts'] as const,
lists: () => [...queryKeys.contacts.all(), 'list'] as const,
list: (filters: ContactFilters) =>
[...queryKeys.contacts.lists(), filters] as const,
details: () => [...queryKeys.contacts.all(), 'detail'] as const,
detail: (id: string) =>
[...queryKeys.contacts.details(), id] as const,
},
orders: {
all: () => ['orders'] as const,
list: (orgId: string, page: number) => ['orders', orgId, page] as const,
detail: (id: string) => ['orders', id] as const,
},
} as const;
// Usage in queries and invalidations:
// useQuery({ queryKey: queryKeys.contacts.list({ page: 1, limit: 20 }) })
// queryClient.invalidateQueries({ queryKey: queryKeys.contacts.lists() })
// — invalidates ALL list queries regardless of filter parameters
Grundlegende Abfragen
// src/hooks/use-contacts.ts
import { useQuery } from '@tanstack/react-query';
import { apiFetch } from '@/lib/api';
import { queryKeys } from '@/lib/query-keys';
interface ContactFilters {
page: number;
limit: number;
search?: string;
status?: string;
}
interface ContactsResponse {
data: Contact[];
total: number;
page: number;
limit: number;
}
export function useContacts(filters: ContactFilters) {
return useQuery({
queryKey: queryKeys.contacts.list(filters),
queryFn: async () => {
const params = new URLSearchParams({
page: String(filters.page),
limit: String(filters.limit),
...(filters.search && { search: filters.search }),
...(filters.status && { status: filters.status }),
});
return apiFetch<ContactsResponse>(`/contacts?${params}`);
},
placeholderData: (previousData) => previousData, // Keep previous page data while fetching
staleTime: 30_000, // Contacts data: 30 second stale time
});
}
export function useContact(id: string) {
return useQuery({
queryKey: queryKeys.contacts.detail(id),
queryFn: () => apiFetch<Contact>(`/contacts/${id}`),
enabled: !!id, // Only fetch if id is provided
});
}
// Usage in a component
'use client';
export default function ContactsPage() {
const [page, setPage] = useState(1);
const [search, setSearch] = useState('');
const { data, isLoading, isError, error, isFetching } = useContacts({
page,
limit: 20,
search: search || undefined,
});
if (isLoading) return <ContactsSkeleton />;
if (isError) return <ErrorState message={error.message} />;
return (
<div>
{isFetching && <RefetchIndicator />} {/* Shows during background refetch */}
<ContactsTable data={data.data} />
<Pagination
page={page}
total={data.total}
limit={20}
onChange={setPage}
/>
</div>
);
}
Mutationen mit Cache-Invalidierung
// src/hooks/use-contact-mutations.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { apiFetch } from '@/lib/api';
import { queryKeys } from '@/lib/query-keys';
import { toast } from '@/components/ui/toast';
export function useCreateContact() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateContactDto) =>
apiFetch<Contact>('/contacts', {
method: 'POST',
body: JSON.stringify(data),
}),
onSuccess: (newContact) => {
// Invalidate all contact list queries — they are now stale
queryClient.invalidateQueries({
queryKey: queryKeys.contacts.lists(),
});
// Seed the detail cache immediately — no need to refetch
queryClient.setQueryData(
queryKeys.contacts.detail(newContact.id),
newContact
);
toast({ title: 'Contact created', variant: 'success' });
},
onError: (error: Error) => {
toast({ title: 'Failed to create contact', description: error.message, variant: 'destructive' });
},
});
}
export function useDeleteContact() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) =>
apiFetch(`/contacts/${id}`, { method: 'DELETE' }),
onSuccess: (_data, deletedId) => {
// Remove from all list queries
queryClient.invalidateQueries({ queryKey: queryKeys.contacts.lists() });
// Remove detail cache
queryClient.removeQueries({ queryKey: queryKeys.contacts.detail(deletedId) });
toast({ title: 'Contact deleted', variant: 'success' });
},
});
}
Optimistische Updates
Optimistische Updates sorgen dafür, dass sich Mutationen sofort anfühlen, indem die Benutzeroberfläche aktualisiert wird, bevor der Server Folgendes bestätigt:
export function useUpdateContact() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: UpdateContactDto }) =>
apiFetch<Contact>(`/contacts/${id}`, {
method: 'PATCH',
body: JSON.stringify(data),
}),
// Called immediately before mutationFn
onMutate: async ({ id, data }) => {
// Cancel any in-flight refetches for this contact
await queryClient.cancelQueries({ queryKey: queryKeys.contacts.detail(id) });
// Snapshot the previous value for rollback
const previousContact = queryClient.getQueryData<Contact>(
queryKeys.contacts.detail(id)
);
// Optimistically update the UI
queryClient.setQueryData<Contact>(
queryKeys.contacts.detail(id),
(old) => old ? { ...old, ...data } : old
);
return { previousContact }; // Passed to onError context
},
onError: (_error, { id }, context) => {
// Roll back to the snapshot on error
if (context?.previousContact) {
queryClient.setQueryData(
queryKeys.contacts.detail(id),
context.previousContact
);
}
toast({ title: 'Update failed', variant: 'destructive' });
},
onSettled: (_data, _error, { id }) => {
// Always refetch after success or error to sync with server truth
queryClient.invalidateQueries({ queryKey: queryKeys.contacts.detail(id) });
},
});
}
Unendliches Scrollen / Paginierung
// src/hooks/use-infinite-contacts.ts
import { useInfiniteQuery } from '@tanstack/react-query';
export function useInfiniteContacts(search?: string) {
return useInfiniteQuery({
queryKey: ['contacts', 'infinite', search],
queryFn: ({ pageParam = 1 }) =>
apiFetch<ContactsResponse>(`/contacts?page=${pageParam}&limit=20${search ? `&search=${search}` : ''}`),
initialPageParam: 1,
getNextPageParam: (lastPage) => {
const nextPage = lastPage.page + 1;
const maxPage = Math.ceil(lastPage.total / lastPage.limit);
return nextPage <= maxPage ? nextPage : undefined;
},
staleTime: 30_000,
});
}
// Infinite scroll component
'use client';
import { useInfiniteContacts } from '@/hooks/use-infinite-contacts';
import { useInView } from 'react-intersection-observer';
import { useEffect } from 'react';
export function InfiniteContactsList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
} = useInfiniteContacts();
const { ref, inView } = useInView({ threshold: 0.5 });
// Fetch next page when the sentinel element enters the viewport
useEffect(() => {
if (inView && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
}, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]);
if (isLoading) return <ContactsSkeleton />;
const contacts = data?.pages.flatMap((page) => page.data) ?? [];
return (
<div>
{contacts.map((contact) => (
<ContactCard key={contact.id} contact={contact} />
))}
{/* Sentinel element — triggers next page load when visible */}
<div ref={ref} className="h-4">
{isFetchingNextPage && <LoadingSpinner />}
</div>
</div>
);
}
Vorabruf für sofortige Navigation
// Prefetch on hover — data is ready before the user clicks
'use client';
import { useQueryClient } from '@tanstack/react-query';
import { Link } from '@/i18n/navigation';
import { queryKeys } from '@/lib/query-keys';
import { apiFetch } from '@/lib/api';
export function ContactRow({ contact }: { contact: Contact }) {
const queryClient = useQueryClient();
const handleMouseEnter = () => {
queryClient.prefetchQuery({
queryKey: queryKeys.contacts.detail(contact.id),
queryFn: () => apiFetch<Contact>(`/contacts/${contact.id}`),
staleTime: 60_000, // Use cached if less than 1 minute old
});
};
return (
<tr onMouseEnter={handleMouseEnter}>
<td>
<Link href={`/dashboard/contacts/${contact.id}`}>{contact.name}</Link>
</td>
<td>{contact.email}</td>
</tr>
);
}
Next.js App Router SSR + TanStack-Abfrage
// src/app/[locale]/dashboard/contacts/page.tsx (Server Component)
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
import { queryKeys } from '@/lib/query-keys';
import { getServerContacts } from '@/lib/server-api';
import { ContactsClient } from './contacts-client';
export default async function ContactsPage({ params }: Props) {
const { locale } = await params;
const queryClient = new QueryClient();
// Prefetch on the server — data is embedded in the HTML
await queryClient.prefetchQuery({
queryKey: queryKeys.contacts.list({ page: 1, limit: 20 }),
queryFn: () => getServerContacts({ page: 1, limit: 20 }),
});
return (
// HydrationBoundary serializes the server QueryClient state
// into the HTML, which the client QueryClient then picks up
<HydrationBoundary state={dehydrate(queryClient)}>
<ContactsClient />
</HydrationBoundary>
);
}
// src/app/[locale]/dashboard/contacts/contacts-client.tsx ('use client')
'use client';
import { useContacts } from '@/hooks/use-contacts';
export function ContactsClient() {
const { data } = useContacts({ page: 1, limit: 20 });
// data is immediately available from the server-dehydrated state
// No loading state on first render — SSR data is used instantly
return <ContactsTable data={data?.data ?? []} />;
}
Häufig gestellte Fragen
Wann sollte ich TanStack Query vs. SWR vs. einen einfachen Abruf verwenden?
Verwenden Sie TanStack Query, wenn Sie Folgendes benötigen: Cache-Ungültigmachung nach Mutationen, optimistische Aktualisierungen, unendliches Scrollen, Vorabruf, erneutes Abrufen im Hintergrund oder Anforderungsdeduplizierung. Verwenden Sie SWR, wenn Sie ein kleineres Paket und nur einfache Anwendungsfälle benötigen. Verwenden Sie einfach fetch in Next.js-Serverkomponenten, wenn Sie das native Daten-Caching von React wünschen (die Funktion cache() und das Muster revalidatePath()). TanStack Query dient der clientseitigen Interaktivität, bei der Benutzeraktionen Auswirkungen auf Serverdaten haben.
Was ist der Unterschied zwischen staleTime und gcTime?
staleTime gibt an, wie lange Daten als aktuell gelten. Während dieser Zeit erfolgt kein erneuter Abruf im Hintergrund und die zwischengespeicherten Daten werden sofort zurückgegeben. Nach Ablauf von staleTime sind die Daten „veraltet“ und werden bei der nächsten Verwendung im Hintergrund erneut abgerufen. gcTime (früher cacheTime) gibt an, wie lange ungenutzte Daten (keine aktiven Abonnenten) vor der Speicherbereinigung im Speicher verbleiben. Legen Sie staleTime basierend darauf fest, wie schnell sich Ihre Daten ändern. Legen Sie gcTime basierend darauf fest, wie lange Sie weg und zurück navigieren möchten, um das Gefühl zu haben, sofort zu sein.
Wie gehe ich global mit Authentifizierungsfehlern (401) um?
Fügen Sie einen globalen Fehlerhandler in Ihren QueryClient-Standardoptionen hinzu. Bei 401 leiten Sie zur Anmeldung weiter und leeren den Abfragecache. In TanStack Query v5: defaultOptions: { queries: { throwOnError: (error) => { if (error.status === 401) { router.push('/auth/login'); queryClient.clear(); return false; } return true; } } }. Alternativ können Sie 401 in Ihrem apiFetch-Helper behandeln, indem Sie eine logout()-Funktion aufrufen.
Wie teile ich den Abfragestatus zwischen Geschwisterkomponenten, ohne Prop-Drilling zu betreiben?
Jede Komponente, die useQuery mit demselben Abfrageschlüssel aufruft, nutzt dieselben zwischengespeicherten Daten – TanStack Query dedupliziert Anfragen automatisch. Zwei ContactsTable-Komponenten und ein ContactCount-Badge, das useContacts({ page: 1, limit: 20 }) mit denselben Filtern aufruft, lesen alle aus einer einzigen zwischengespeicherten Anforderung. Kein Schraubenbohren, kein Kontext erforderlich.
Sollten Abfrageschlüssel die Sitzungs- oder Organisations-ID des Benutzers enthalten?
Ja – wenn die Daten auf einen Benutzer oder eine Organisation beschränkt sind, schließen Sie die Bereichskennung in den Abfrageschlüssel ein. Dadurch wird verhindert, dass die Daten eines Benutzers für einen anderen angezeigt werden (kritisch für Multi-Tenant-Apps). Leeren Sie beim Abmelden außerdem den gesamten Abfragecache: queryClient.clear() oder queryClient.removeQueries() – andernfalls blinken möglicherweise veraltete Daten aus der vorherigen Sitzung kurz für den nächsten Benutzer.
Nächste Schritte
TanStack Query verwandelt das Abrufen von React-Daten von einem Zustandsverwaltungsproblem in ein deklaratives, Cache-fähiges System. Optimistische Updates, Prefetching und SSR-Hydratation bieten Ihren Benutzern ein sofortiges Erlebnis – selbst bei langsamen Verbindungen.
ECOSIRE erstellt Next.js-Admin-Panels und Kundenportale mit TanStack Query für den gesamten Serverstatus, kombiniert mit Zustand für den reinen Client-UI-Status und TanStack Table für datenintensive Ansichten. [Entdecken Sie unsere Frontend-Engineering-Dienste] (/services), um zu erfahren, wie wir leistungsstarke, produktionsreife React-Anwendungen erstellen.
Geschrieben von
ECOSIRE Research and Development Team
Entwicklung von Enterprise-Digitalprodukten bei ECOSIRE. Einblicke in Odoo-Integrationen, E-Commerce-Automatisierung und KI-gestützte Geschäftslösungen.
Verwandte Artikel
Next.js 16 App Router: Production Patterns and Pitfalls
Production-ready Next.js 16 App Router patterns: server components, caching strategies, metadata API, error boundaries, and performance pitfalls to avoid.
Nginx Production Configuration: SSL, Caching, and Security
Nginx production configuration guide: SSL termination, HTTP/2, caching headers, security headers, rate limiting, reverse proxy setup, and Cloudflare integration patterns.
Optimizing AI Agent Costs: Token Usage and Caching
Practical strategies for reducing AI agent operational costs through token optimization, caching, model routing, and usage monitoring. Real savings from production OpenClaw deployments.
Mehr aus Performance & Scalability
k6 Load Testing: Stress-Test Your APIs Before Launch
Master k6 load testing for Node.js APIs. Covers virtual user ramp-ups, thresholds, scenarios, HTTP/2, WebSocket testing, Grafana dashboards, and CI integration patterns.
Nginx Production Configuration: SSL, Caching, and Security
Nginx production configuration guide: SSL termination, HTTP/2, caching headers, security headers, rate limiting, reverse proxy setup, and Cloudflare integration patterns.
Odoo Performance Tuning: PostgreSQL and Server Optimization
Expert guide to Odoo 19 performance tuning. Covers PostgreSQL configuration, indexing, query optimization, Nginx caching, and server sizing for enterprise deployments.
Odoo vs Acumatica: Cloud ERP for Growing Businesses
Odoo vs Acumatica compared for 2026: unique pricing models, scalability, manufacturing depth, and which cloud ERP fits your growth trajectory.
Testing and Monitoring AI Agents in Production
A complete guide to testing and monitoring AI agents in production environments. Covers evaluation frameworks, observability, drift detection, and incident response for OpenClaw deployments.
Compliance Monitoring Agents with OpenClaw
Deploy OpenClaw AI agents for continuous compliance monitoring. Automate regulatory checks, policy enforcement, audit trail generation, and compliance reporting.