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>
504 lines
12 KiB
Markdown
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)
|