TanStack Query: Daten ohne Boilerplate
Im letzten Kapitel brauchten wir für einen einzigen Datenabruf drei State-Variablen, manuelles Reset, einen AbortController und eine Cleanup-Funktion — und Retry und Caching fehlten trotzdem. TanStack Query löst all das mit einer einzigen Funktion.
pnpm add @tanstack/react-queryTanStack Query braucht einen QueryClient — das zentrale Cache-Objekt — und einen QueryClientProvider, der ihn in den React-Baum bringt:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'import { StrictMode } from 'react'import { createRoot } from 'react-dom/client'import App from './App'
const queryClient = new QueryClient()
createRoot(document.getElementById('root')!).render( <StrictMode> <QueryClientProvider client={queryClient}> <App /> </QueryClientProvider> </StrictMode>)Der QueryClientProvider kommt an die Wurzel der App — alle Komponenten darunter können useQuery und useMutation verwenden.
useQuery: Daten laden
Abschnitt betitelt „useQuery: Daten laden“Derselbe Todo-Detail-Abruf aus dem letzten Kapitel — jetzt mit useQuery:
import { useQuery } from '@tanstack/react-query'
type Todo = { id: number; title: string; completed: boolean; userId: number }
async function fetchTodo(id: number): Promise<Todo> { const res = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`) if (!res.ok) throw new Error(`HTTP-Fehler: ${res.status}`) return res.json()}
export default function TodoDetail({ id }: { id: number }) { const { data: todo, isLoading, isError, error } = useQuery({ queryKey: ['todo', id], queryFn: () => fetchTodo(id), })
if (isLoading) return <p>Lade...</p> if (isError) return <p>Fehler: {error.message}</p>
return ( <div> <h2>{todo!.title}</h2> <p>{todo!.completed ? 'Erledigt' : 'Offen'}</p> </div> )}Weg: 3 useState-Aufrufe, setIsLoading, setError(null), AbortController, finally, Cleanup-Funktion. Übrig: ein useQuery-Aufruf.
Was TanStack Query dabei automatisch übernimmt:
- Loading-State beim ersten Laden
- Fehlerbehandlung inkl. Fehlerobjekt
- Abort laufender Requests bei Komponentenunmount
- Caching: Wenn
['todo', 5]schon im Cache ist, wird sofort der gecachte Wert angezeigt — kein Flackern - Background Refetch: Wenn der Nutzer den Tab wechselt und zurückkommt, holt TanStack Query still neue Daten
- Stale Time: Konfigurierbar, wie lange Daten als “frisch” gelten, bevor neu geladen wird
Der queryKey
Abschnitt betitelt „Der queryKey“Der queryKey ist der Cache-Schlüssel. Er ist ein Array — üblicherweise ein Name plus die Parameter, von denen die Anfrage abhängt:
// Alle TodosqueryKey: ['todos']
// Ein einzelnes Todo mit einer bestimmten IDqueryKey: ['todo', id]
// Gefilterte TodosqueryKey: ['todos', { filter: 'open' }]Ändert sich ein Wert im queryKey, führt TanStack Query automatisch eine neue Anfrage aus. Das ersetzt die [id]-Dependency aus useEffect.
TypeScript
Abschnitt betitelt „TypeScript“useQuery nimmt ein Generic für den Rückgabetyp der queryFn:
const { data } = useQuery<Todo>({ queryKey: ['todo', id], queryFn: () => fetchTodo(id),})Wenn fetchTodo bereits Promise<Todo> zurückgibt, leitet TypeScript den Typ auch ohne explizites Generic ab — data ist dann Todo | undefined (undefined solange isLoading true ist).
Das ! in todo!.title lässt sich mit einem Early Exit vermeiden:
if (!todo) return nullreturn <h2>{todo.title}</h2> // todo ist hier sicher TodoListe laden
Abschnitt betitelt „Liste laden“async function fetchTodos(): Promise<Todo[]> { const res = await fetch('https://jsonplaceholder.typicode.com/todos?_limit=20') if (!res.ok) throw new Error(`HTTP-Fehler: ${res.status}`) return res.json()}
export default function TodoListe() { const { data: todos, isLoading, isError } = useQuery<Todo[]>({ queryKey: ['todos'], queryFn: fetchTodos, })
if (isLoading) return <p>Lade...</p> if (isError) return <p>Fehler beim Laden.</p>
return ( <ul> {todos!.map(todo => ( <li key={todo.id}>{todo.title}</li> ))} </ul> )}useMutation: Daten schreiben
Abschnitt betitelt „useMutation: Daten schreiben“Für schreibende Operationen (POST, PATCH, DELETE) gibt es useMutation. Nach einer erfolgreichen Mutation soll der Cache invalidiert werden — damit TanStack Query die Liste automatisch neu lädt:
import { useMutation, useQueryClient } from '@tanstack/react-query'
async function createTodo(title: string): Promise<Todo> { const res = await fetch('https://jsonplaceholder.typicode.com/todos', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title, completed: false, userId: 1 }), }) if (!res.ok) throw new Error(`HTTP-Fehler: ${res.status}`) return res.json()}
export function TodoFormular() { const queryClient = useQueryClient()
const mutation = useMutation({ mutationFn: createTodo, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['todos'] }) }, })
function handleSubmit(e: React.FormEvent<HTMLFormElement>) { e.preventDefault() const form = e.currentTarget const title = new FormData(form).get('title') as string mutation.mutate(title) form.reset() }
return ( <form onSubmit={handleSubmit}> <input name="title" placeholder="Neue Aufgabe..." required /> <button type="submit" disabled={mutation.isPending}> {mutation.isPending ? 'Speichern...' : 'Hinzufügen'} </button> {mutation.isError && <p>Fehler: {mutation.error.message}</p>} </form> )}invalidateQueries({ queryKey: ['todos'] }) markiert alle gecachten Queries mit dem Schlüssel ['todos'] als veraltet. TanStack Query lädt sie beim nächsten Zugriff neu — ohne dass wir etwas koordinieren müssen.
Stale Time konfigurieren
Abschnitt betitelt „Stale Time konfigurieren“Standardmäßig gelten Daten sofort als veraltet (“stale”) und werden beim nächsten Fensterfokus neu geladen. Das lässt sich global oder pro Query anpassen:
// Global für alle Queries: 60 Sekunden frischconst queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 60 * 1000, }, },})
// Oder pro QueryuseQuery({ queryKey: ['todos'], queryFn: fetchTodos, staleTime: 60 * 1000,})useEffect vs. useQuery
Abschnitt betitelt „useEffect vs. useQuery“useEffect | useQuery | |
|---|---|---|
| Loading State | manuell | eingebaut |
| Error State | manuell | eingebaut |
| Caching | nein | ja |
| Background Refetch | nein | ja |
| Race Conditions | manuell (AbortController) | automatisch |
| Retry bei Fehler | manuell | automatisch (3×) |
| Code pro Datenabruf | ~20–30 Zeilen | ~5 Zeilen |
useEffect für Datenabruf ist keine falsche Wahl — es ist nützlich, es einmal verstanden zu haben. Aber für alles, was über einen einmaligen Abruf hinausgeht, ist TanStack Query die bessere Wahl.