Was ist TanStack Query?
TanStack Query, früher bekannt als React Query, ist eine Library für das Server-State-Management. Sie geht davon aus, dass Daten, die von einer API abgerufen werden, sich grundlegend von dem State unterscheiden, den man in useState verwaltet. Server-Daten werden geteilt, gecacht, können veralten und müssen erneut abgerufen werden. Dies mit Effects und Flags zu steuern, ist mühsam und fehleranfällig.
TanStack Query bietet Server-Daten einen ordentlichen Cache. Sie beschreiben, wie ein bestimmter Datensatz abgerufen wird, und weisen ihm einen Key zu; die Library kümmert sich dann um Caching, Deduplizierung, Loading- und Error-States, Background-Refetching sowie die Invalidierung. Es ist die fehlende Data-Layer für React und lässt sich mit jeder Client-State-Library kombinieren.
Queries
Eine Query wird durch einen Key und eine Query-Funktion definiert. Der Key identifiziert den Cache-Eintrag; die Funktion ruft die Daten ab.
// api.ts
async function fetchPost(id: string) {
const res = await fetch(`/api/posts/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
// Post.tsx
import { useQuery } from "@tanstack/react-query";
export function Post({ id }: { id: string }) {
const { data, isLoading, isError, error } = useQuery({
queryKey: ["post", id],
queryFn: () => fetchPost(id),
});
if (isLoading) return <p>Loading…</p>;
if (isError) return <p>Error: {error.message}</p>;
return <article>{data.title}</article>;
}
Jede Komponente, die denselben Key verwendet, teilt sich einen Cache-Eintrag. Wenn die Komponente also zweimal gemountet wird, erfolgt der Abruf nur einmal. Die Status-Flags werden bereitgestellt, sodass du diese niemals selbst schreiben musst.
Query-Keys und der Cache
Der Key ist die Identität des Cache-Eintrags und sollte jede Variable enthalten, von der der Fetch abhängt.
// keys.ts
useQuery({ queryKey: ["posts"], queryFn: fetchPosts });
useQuery({ queryKey: ["post", id], queryFn: () => fetchPost(id) });
useQuery({
queryKey: ["posts", { page, status }],
queryFn: () => fetchPosts({ page, status }),
});
Da Keys Arrays sind, können Sie eine ganze Gruppe mithilfe eines Präfixes invalidieren. Das Invalidieren von ["posts"] invalidiert gleichzeitig auch ["posts", { page: 1 }], was Cache-Updates vorhersehbar macht.
Aktualität und Refetching
Zwei Optionen steuern, wann Daten erneut abgerufen (refetched) werden.
// freshness.ts
useQuery({
queryKey: ["posts"],
queryFn: fetchPosts,
staleTime: 60_000, // fresh for one minute
gcTime: 5 * 60_000, // keep unused data for five minutes
});
staleTime legt fest, wie lange Daten als aktuell (fresh) betrachtet werden; solange sie aktuell sind, erfolgt kein Refetch im Hintergrund. gcTime bestimmt, wie lange ein nicht verwendeter Eintrag im Speicher bleibt, bevor er durch die Garbage Collection entfernt wird. Standardmäßig führen Queries beim Mounten, beim Fokus auf das Fenster und beim erneuten Verbindungsaufbau einen Refetch durch, wodurch die UI ohne manuellen Aufwand aktuell bleibt.
Mutations
useMutation kümmert sich um Schreibvorgänge: Erstellen, Aktualisieren und Löschen.
// AddPost.tsx
import { useMutation, useQueryClient } from "@tanstack/react-query";
export function AddPost() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (title: string) =>
fetch("/api/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title }),
}).then((r) => r.json()),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["posts"] });
},
});
return (
<button
onClick={() => mutation.mutate("New post")}
disabled={mutation.isPending}
>
{mutation.isPending ? "Saving…" : "Add post"}
</button>
);
}
Nach einer erfolgreichen Mutation signalisiert das Invalidieren der betroffenen Queries allen Consumern, dass die Daten neu geladen werden müssen. So bleibt der Server die „Source of Truth“ und manuelle Eingriffe in den Cache werden vermieden.
Optimistische Updates
Bei Aktionen, die sich unmittelbar anfühlen sollen, aktualisieren Sie den Cache, bevor der Server antwortet, und machen Sie die Änderungen im Fehlerfall rückgängig.
// optimistic.ts
useMutation({
mutationFn: toggleLike,
onMutate: async (postId) => {
await queryClient.cancelQueries({ queryKey: ["post", postId] });
const previous = queryClient.getQueryData(["post", postId]);
queryClient.setQueryData(["post", postId], (old) => ({
...old,
likes: old.likes + 1,
}));
return { previous };
},
onError: (_err, postId, context) => {
queryClient.setQueryData(["post", postId], context.previous);
},
onSettled: (_data, _err, postId) => {
queryClient.invalidateQueries({ queryKey: ["post", postId] });
},
});
onMutate wendet die optimistische Änderung an und erstellt einen Snapshot des vorherigen Wertes, onError führt den Rollback durch und onSettled ruft die Daten erneut ab, um sie mit dem Server zu synchronisieren.
Pagination und Infinite Queries
TanStack Query bietet erstklassigen Support für paginierte und unendliche Daten. useQuery mit einer keepPreviousData-Option sorgt für reibungslose Seitenübergänge, und useInfiniteQuery verwaltet cursor-basierte Listen mithilfe einer fetchNextPage-Funktion. Da jede Seite Teil des Caches ist, erfolgt die Navigation vor und zurück augenblicklich.
Server-State gegenüber Client-State
Dies ist die Unterscheidung, durch die TanStack Query so richtig Sinn ergibt:
- Server-State wird vom Server verwaltet, ist gemeinsam genutzt, asynchron und kann veralten. Hierfür nutzt man TanStack Query.
- Client-State wird vom Browser verwaltet, ist synchron und lokal. Hierfür nutzt man
useState, Context, Zustand oder Redux.
Beide in einem einzigen Store zu vermischen, ist die Quelle für viel Komplexität. Die Trennung – TanStack Query für die Daten, ein kleiner Store für den UI-State – hält beides simpel.
Best Practices
- Nehmen Sie jede Variable in den Query Key auf.
- Setzen Sie
staleTimebewusst; der Standardwert Null führt zu sehr häufigen Refetches. - Nutzen Sie Mutations in Kombination mit Invalidation, anstatt den Cache überall manuell zu aktualisieren.
- Halten Sie Query-Funktionen klein und platzieren Sie diese direkt beim entsprechenden Query.
- Implementieren Sie Optimistic Updates nur dort, wo ein sofortiges Feedback entscheidend ist.
- Nutzen Sie die Devtools, um den Cache-Status während der Entwicklung zu untersuchen.
- Trennen Sie den Server-State in TanStack Query vom Client-State.
Häufige Fehler
- Verwendung desselben Keys für verschiedene Inputs, was zu veralteten oder falschen Daten führt.
staleTimeauf Null lassen, wodurch ständige Refetches ausgelöst werden.- Duplizieren von Server-Daten in einem globalen Store.
- Vergessen, nach einer Mutation zu invalidieren, wodurch veraltete Daten angezeigt werden.
- Schreiben von Optimistic Updates ohne einen Rollback-Pfad.
- Behandeln von Query-Funktionen wie Komponenten und Aufrufen von Hooks innerhalb dieser Funktionen.
Wie geht es weiter?
TanStack Query ist der Standardweg für die Verwaltung von Serverdaten in modernen Apps. Kombinieren Sie es mit der Fetch API für den Request-Layer, einem Client-Store wie Zustand für den UI-State und Redux Toolkit, falls Sie einen strikten globalen State benötigen. Für Vue übernimmt Pinia den Client-State, während TanStack Query die Serverseite abdeckt.