# 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 ( {children} ); } ``` --- ## Basic Queries ### useQuery ```typescript import { useQuery } from '@tanstack/react-query'; // API Function async function fetchUser(userId: string): Promise { 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 ; if (isError) return ; return (

{user.name}

{isFetching && Updating...}
); } ``` ### 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 (