Files
Portfolio/blog-posts/37-tanstack-query.md
T
damjan_savicandClaude Opus 4.5 e1bbe5455d Initial commit: Portfolio Website
Vollständige Next.js 15 Portfolio-Website mit:
- Blog-System mit 100+ Artikeln
- Supabase-Integration
- Responsive Design mit Tailwind CSS
- TypeScript-Konfiguration
- Testing-Setup mit Vitest und Playwright

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 15:07:20 +01:00

504 lines
12 KiB
Markdown

# 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
```typescript
// 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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
// 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
```typescript
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
```typescript
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
```typescript
// 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
```typescript
// 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:
1. **Automatisches Caching**: Keine manuelle Cache-Logik
2. **Background Refetch**: Daten bleiben frisch
3. **Optimistic Updates**: Schnelle UX
4. **DevTools**: Debugging leicht gemacht
Für jede App mit Server-Daten ist TanStack Query unverzichtbar.
---
## Bildprompts
1. "Data flowing from server to UI with caching layer in between, architectural diagram"
2. "Stale data refreshing in background, real-time update visualization"
3. "Query cache tree with multiple components accessing same data, efficiency concept"
---
## Quellen
- [TanStack Query Documentation](https://tanstack.com/query)
- [TanStack Query GitHub](https://github.com/TanStack/query)
- [Practical React Query](https://tkdodo.eu/blog/practical-react-query)
- [React Query DevTools](https://tanstack.com/query/latest/docs/framework/react/devtools)