Zum Inhalt springen

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.

Terminal-Fenster
pnpm add @tanstack/react-query

TanStack Query braucht einen QueryClient — das zentrale Cache-Objekt — und einen QueryClientProvider, der ihn in den React-Baum bringt:

main.tsx
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.

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 ist der Cache-Schlüssel. Er ist ein Array — üblicherweise ein Name plus die Parameter, von denen die Anfrage abhängt:

// Alle Todos
queryKey: ['todos']
// Ein einzelnes Todo mit einer bestimmten ID
queryKey: ['todo', id]
// Gefilterte Todos
queryKey: ['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.

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 null
return <h2>{todo.title}</h2> // todo ist hier sicher Todo
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>
)
}

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.

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 frisch
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
},
},
})
// Oder pro Query
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 60 * 1000,
})
useEffectuseQuery
Loading Statemanuelleingebaut
Error Statemanuelleingebaut
Cachingneinja
Background Refetchneinja
Race Conditionsmanuell (AbortController)automatisch
Retry bei Fehlermanuellautomatisch (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.