1 min read
Web DevelopmentTanStack Query: Intelligentes Data Fetching für React
TanStack Query (React Query) für Server State Management. Caching, Background Refetch, Optimistic Updates und Best Practices.
TanStack QueryReact QueryData FetchingServer StateCachingReact

TanStack Query: Intelligentes Data Fetching für React
Meta-Description: TanStack Query (React Query) für Server State Management. Caching, Background Refetch, Optimistic Updates und Best Practices.
Keywords: TanStack Query, React Query, Data Fetching, Server State, Caching, React, API Client, SWR
Einführung
TanStack Query (ehemals React Query) löst das schwerste Problem in React: Server State Management. Statt manuelles Fetching, Loading States und Caching zu bauen, bietet es eine deklarative, caching-first Lösung.
Warum TanStack Query?
┌─────────────────────────────────────────────────────────────┐
│ SERVER STATE CHALLENGES │
├─────────────────────────────────────────────────────────────┤
│ │
│ Ohne TanStack Query: │
│ ├── Manuelles Loading/Error State │
│ ├── Keine automatische Cache-Invalidierung │
│ ├── Duplizierte Requests │
│ ├── Kein Background Refetch │
│ ├── Komplizierte Pagination │
│ └── Race Conditions │
│ │
│ Mit TanStack Query: │
│ ├── Automatisches Loading/Error Handling │
│ ├── Intelligentes Caching │
│ ├── Request Deduplication │
│ ├── Stale-While-Revalidate │
│ ├── Eingebaute Pagination/Infinite Scroll │
│ └── Automatische Retry-Logik │
│ │
└─────────────────────────────────────────────────────────────┘Setup
// 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 QueryProvider({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 Minute
gcTime: 5 * 60 * 1000, // 5 Minuten (früher cacheTime)
retry: 3,
refetchOnWindowFocus: true,
refetchOnReconnect: true
}
}
}));
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}Basic Queries
useQuery
import { useQuery } from '@tanstack/react-query';
// API Function
async function fetchUser(userId: string): Promise<User> {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('Failed to fetch user');
return response.json();
}
// Component
function UserProfile({ userId }: { userId: string }) {
const {
data: user,
isLoading,
isError,
error,
isFetching, // true auch bei Background Refetch
isStale, // Daten sind "veraltet"
refetch // Manueller Refetch
} = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
enabled: !!userId, // Nur fetchen wenn userId existiert
staleTime: 5 * 60 * 1000,
placeholderData: previousUser // Zeige alte Daten während Refetch
});
if (isLoading) return <Skeleton />;
if (isError) return <Error message={error.message} />;
return (
<div>
<h1>{user.name}</h1>
{isFetching && <span>Updating...</span>}
</div>
);
}Query mit abhängigen Daten
function UserPosts({ userId }: { userId: string }) {
// Erst User laden
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId)
});
// Dann Posts (nur wenn User da)
const { data: posts } = useQuery({
queryKey: ['posts', user?.id],
queryFn: () => fetchUserPosts(user!.id),
enabled: !!user // Wartet auf User
});
return (/* ... */);
}Mutations
import { useMutation, useQueryClient } from '@tanstack/react-query';
function CreatePostForm() {
const queryClient = useQueryClient();
const createPost = useMutation({
mutationFn: async (newPost: CreatePostInput) => {
const response = await fetch('/api/posts', {
method: 'POST',
body: JSON.stringify(newPost)
});
if (!response.ok) throw new Error('Failed to create post');
return response.json();
},
// Bei Erfolg: Cache invalidieren
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
// Oder: Optimistic Update
onMutate: async (newPost) => {
// Laufende Queries canceln
await queryClient.cancelQueries({ queryKey: ['posts'] });
// Snapshot des aktuellen States
const previousPosts = queryClient.getQueryData(['posts']);
// Optimistisch updaten
queryClient.setQueryData(['posts'], (old: Post[]) => [
{ id: 'temp', ...newPost },
...old
]);
return { previousPosts };
},
onError: (err, newPost, context) => {
// Bei Fehler: Rollback
queryClient.setQueryData(['posts'], context?.previousPosts);
},
onSettled: () => {
// Immer am Ende: Refetch für Konsistenz
queryClient.invalidateQueries({ queryKey: ['posts'] });
}
});
const handleSubmit = (data: FormData) => {
createPost.mutate({
title: data.get('title') as string,
content: data.get('content') as string
});
};
return (
<form onSubmit={handleSubmit}>
<input name="title" />
<textarea name="content" />
<button disabled={createPost.isPending}>
{createPost.isPending ? 'Erstellen...' : 'Post erstellen'}
</button>
{createPost.isError && <Error message={createPost.error.message} />}
</form>
);
}Pagination & Infinite Scroll
Pagination
function PaginatedPosts() {
const [page, setPage] = useState(1);
const { data, isPlaceholderData } = useQuery({
queryKey: ['posts', page],
queryFn: () => fetchPosts(page),
placeholderData: keepPreviousData // Zeigt alte Daten während neuer Page lädt
});
return (
<div>
{data?.posts.map(post => <PostCard key={post.id} post={post} />)}
<div>
<button
onClick={() => setPage(p => Math.max(p - 1, 1))}
disabled={page === 1}
>
Zurück
</button>
<span>Seite {page}</span>
<button
onClick={() => setPage(p => p + 1)}
disabled={isPlaceholderData || !data?.hasMore}
>
Weiter
</button>
</div>
</div>
);
}Infinite Scroll
import { useInfiniteQuery } from '@tanstack/react-query';
import { useInView } from 'react-intersection-observer';
function InfinitePosts() {
const { ref, inView } = useInView();
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage
} = useInfiniteQuery({
queryKey: ['posts', 'infinite'],
queryFn: ({ pageParam }) => fetchPosts(pageParam),
initialPageParam: 1,
getNextPageParam: (lastPage) =>
lastPage.hasMore ? lastPage.nextPage : undefined
});
// Auto-load bei Scroll
useEffect(() => {
if (inView && hasNextPage) {
fetchNextPage();
}
}, [inView, hasNextPage, fetchNextPage]);
return (
<div>
{data?.pages.map(page =>
page.posts.map(post => <PostCard key={post.id} post={post} />)
)}
<div ref={ref}>
{isFetchingNextPage
? <Spinner />
: hasNextPage
? 'Mehr laden...'
: 'Keine weiteren Posts'}
</div>
</div>
);
}Prefetching
// Hover Prefetch
function PostLink({ postId }: { postId: string }) {
const queryClient = useQueryClient();
const prefetchPost = () => {
queryClient.prefetchQuery({
queryKey: ['post', postId],
queryFn: () => fetchPost(postId),
staleTime: 60 * 1000
});
};
return (
<Link
href={`/posts/${postId}`}
onMouseEnter={prefetchPost}
onFocus={prefetchPost}
>
Post anzeigen
</Link>
);
}
// Server-Side Prefetch (Next.js)
export async function getServerSideProps() {
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: fetchPosts
});
return {
props: {
dehydratedState: dehydrate(queryClient)
}
};
}Query Invalidation
const queryClient = useQueryClient();
// Einzelnen Query invalidieren
queryClient.invalidateQueries({ queryKey: ['user', userId] });
// Alle User-Queries
queryClient.invalidateQueries({ queryKey: ['user'] });
// Alle Queries
queryClient.invalidateQueries();
// Mit Predicate
queryClient.invalidateQueries({
predicate: (query) =>
query.queryKey[0] === 'post' &&
(query.queryKey[1] as Post)?.authorId === userId
});
// Nur refetchen wenn aktiv (sichtbar)
queryClient.invalidateQueries({
queryKey: ['posts'],
refetchType: 'active' // 'all' | 'active' | 'inactive' | 'none'
});Suspense Mode
import { useSuspenseQuery } from '@tanstack/react-query';
function UserProfile({ userId }: { userId: string }) {
// Wirft Promise während Loading (für Suspense)
const { data: user } = useSuspenseQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId)
});
// user ist garantiert da (kein undefined)
return <div>{user.name}</div>;
}
// Parent
function UserPage({ userId }: { userId: string }) {
return (
<Suspense fallback={<Skeleton />}>
<UserProfile userId={userId} />
</Suspense>
);
}Custom Hooks Pattern
// hooks/useUser.ts
export function useUser(userId: string) {
return useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
enabled: !!userId
});
}
export function useUpdateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateUser,
onSuccess: (data, variables) => {
queryClient.setQueryData(['user', variables.id], data);
}
});
}
// Verwendung
function Profile({ userId }: { userId: string }) {
const { data: user, isLoading } = useUser(userId);
const updateUser = useUpdateUser();
// ...
}DevTools & Debugging
// Query Logging
const queryClient = new QueryClient({
defaultOptions: {
queries: {
onError: (error) => {
console.error('Query error:', error);
// Sentry, LogRocket, etc.
}
},
mutations: {
onError: (error) => {
console.error('Mutation error:', error);
}
}
}
});
// DevTools
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools
initialIsOpen={false}
buttonPosition="bottom-right"
/>
</QueryClientProvider>Fazit
TanStack Query bietet:
- Automatisches Caching: Keine manuelle Cache-Logik
- Background Refetch: Daten bleiben frisch
- Optimistic Updates: Schnelle UX
- DevTools: Debugging leicht gemacht
Für jede App mit Server-Daten ist TanStack Query unverzichtbar.
Bildprompts
- "Data flowing from server to UI with caching layer in between, architectural diagram"
- "Stale data refreshing in background, real-time update visualization"
- "Query cache tree with multiple components accessing same data, efficiency concept"