useApiResource als Muster, bevor TanStack Query ins Spiel kommt
Loading-Spinner, Fehlermeldung, Daten: Dieses Dreieck aus State tippt sich in jeder Komponente, die Daten von einer API lädt, fast identisch. Ein eigener Custom Hook bündelt dieses Muster an einer Stelle, bevor er automatisch zu jeder Komponente wandert, die ihn braucht, und zeigt gleichzeitig, wo der Eigenbau endet und eine Bibliothek wie TanStack Query beginnt.
Inhaltsverzeichnis
- 1. Warum sich Datenabruf-Logik über Komponenten hinweg wiederholt
- 2. Das Grundgerüst von useApiResource
- 3. Race Conditions beim schnellen Wechsel der URL vermeiden
- 4. Varianten für POST-Requests und abhängige Abfragen
- 5. Wo der Eigenbau an seine Grenzen stößt
- 6. Wann TanStack Query klar überlegen ist
- 7. Custom Hooks isoliert testen
- 8. Typsicherheit mit generischen Hooks
- 9. Die Entscheidung praktisch treffen
- 10. Zusammenfassung
- 11. FAQ
1. Warum sich Datenabruf-Logik über Komponenten hinweg wiederholt
Fast jede Komponente, die Daten von einer API lädt, braucht dieselben drei Zustände: einen Ladezustand, während die Anfrage läuft, einen Fehlerzustand, falls sie fehlschlägt, und die eigentlichen Daten, sobald die Antwort da ist. Ohne eine gemeinsame Abstraktion landet dieses Dreieck aus drei useState-Aufrufen und einem useEffect in jeder einzelnen Komponente neu, die irgendetwas von einem Server lädt, mit kleinen, unbeabsichtigten Abweichungen zwischen den Kopien, die sich über die Zeit ansammeln.
Diese Abweichungen sind das eigentliche Problem, nicht die Tipparbeit. Eine Komponente vergisst, den Fehlerzustand bei einem erneuten Request zurückzusetzen, eine andere ruft setState nach dem Unmount der Komponente auf und produziert eine Warnung in der Konsole, eine dritte behandelt einen abgebrochenen Request wie einen echten Fehler. Ein Custom Hook zieht genau diese Logik an eine einzige, testbare Stelle, sodass ein Fix oder eine Verbesserung an einem Ort passiert und automatisch überall wirkt, wo der Hook verwendet wird.
2. Das Grundgerüst von useApiResource
Der Hook useApiResource nimmt eine URL oder eine Abruf-Funktion entgegen und gibt ein Objekt mit data, error, isLoading und einer refetch-Funktion zurück. Intern verwaltet er drei State-Werte über useState und einen useEffect, der bei jeder Änderung der URL neu ausgeführt wird. Wichtig ist, den Ladezustand vor dem eigentlichen Fetch-Aufruf zu setzen und den Fehlerzustand bei jedem neuen Versuch explizit zurückzusetzen, sonst bleibt eine alte Fehlermeldung sichtbar, während bereits neue Daten geladen werden.
Genauso wichtig ist die Reihenfolge der State-Updates nach Abschluss des Requests: Erst die Daten setzen, dann den Ladezustand beenden, damit eine Komponente niemals einen Zwischenzustand mit isLoading false und leeren Daten rendert. Diese Details wirken klein, sind aber genau die Stellen, an denen handgeschriebene Fetch-Logik in einzelnen Komponenten typischerweise auseinanderdriftet, weil sie an jeder Stelle leicht anders implementiert werden.
function useApiResource(url) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [reloadToken, setReloadToken] = useState(0);
useEffect(() => {
let cancelled = false;
async function load() {
setIsLoading(true);
setError(null);
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Request fehlgeschlagen: ${response.status}`);
}
const json = await response.json();
if (!cancelled) {
setData(json);
}
} catch (err) {
if (!cancelled) {
setError(err);
}
} finally {
if (!cancelled) {
setIsLoading(false);
}
}
}
load();
return () => {
cancelled = true;
};
}, [url, reloadToken]);
const refetch = useCallback(() => setReloadToken((t) => t + 1), []);
return { data, error, isLoading, refetch };
}
3. Race Conditions beim schnellen Wechsel der URL vermeiden
Ein Fehler, der in handgeschriebener Fetch-Logik besonders häufig übersehen wird, betrifft schnell aufeinanderfolgende Anfragen: Wechselt ein Nutzer etwa in einem Suchfeld schnell zwischen mehreren Begriffen, starten mehrere Requests parallel, die aber nicht garantiert in der Reihenfolge zurückkommen, in der sie losgeschickt wurden. Ohne Schutzmechanismus kann eine ältere, langsamere Antwort eine bereits aktuellere überschreiben, und die Oberfläche zeigt am Ende die Daten der falschen, veralteten Anfrage.
Der cancelled-Flag im useEffect-Cleanup in useApiResource löst genau dieses Problem: Sobald der Effekt erneut ausgeführt wird, etwa weil sich die URL geändert hat, markiert React die vorherige Ausführung als veraltet und ruft die Cleanup-Funktion auf, die cancelled auf true setzt. Trifft die Antwort des alten Requests danach trotzdem noch ein, wird sie stillschweigend verworfen, statt den State zu überschreiben. Das ist einer der Hauptgründe, warum ein zentraler Hook robuster ist als über mehrere Komponenten verstreute Kopien derselben Logik.
4. Varianten für POST-Requests und abhängige Abfragen
Das Grundmuster lässt sich leicht auf Mutationen erweitern, etwa einen useApiMutation-Hook, der eine asynchrone Funktion entgegennimmt und diese erst bei Aufruf einer zurückgegebenen execute-Funktion ausführt, statt automatisch beim Mount. Das passt zu Formularen und Buttons, bei denen eine Anfrage explizit durch eine Nutzeraktion ausgelöst werden soll, während useApiResource für Daten gedacht ist, die direkt beim Rendern einer Komponente benötigt werden.
Abhängige Abfragen, bei denen ein zweiter Request erst nach erfolgreichem ersten Request starten darf, etwa Nutzerdetails erst nach Erhalt einer Nutzer-ID, lassen sich lösen, indem die URL des zweiten useApiResource-Aufrufs bedingt auf null gesetzt wird, solange die Voraussetzung fehlt. Der Hook prüft dann intern, ob eine gültige URL vorliegt, und überspringt den Fetch-Aufruf andernfalls, was die Abhängigkeitskette explizit und ohne verschachtelte useEffect-Konstruktionen lesbar macht.
function useApiMutation(mutationFn) {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const execute = useCallback(
async (payload) => {
setIsLoading(true);
setError(null);
try {
return await mutationFn(payload);
} catch (err) {
setError(err);
throw err;
} finally {
setIsLoading(false);
}
},
[mutationFn]
);
return { execute, isLoading, error };
}
// Abhaengige Abfrage: erst laden, sobald userId vorhanden ist
function UserDetails({ userId }) {
const { data: user } = useApiResource(userId ? `/api/users/${userId}` : null);
return user ? <p>{user.name}</p> : null;
}
5. Wo der Eigenbau an seine Grenzen stößt
Ein selbstgebauter Hook wie useApiResource löst die drei Kernprobleme Loading, Error und Race Conditions solide, deckt aber bewusst nicht ab, was eine ausgewachsene Datenabruf-Bibliothek zusätzlich mitbringt. Dazu zählen ein geteilter Cache über mehrere Komponenten hinweg, sodass zwei Komponenten, die dieselbe URL laden, nicht zweimal denselben Request auslösen, automatisches erneutes Laden beim Zurückkehren ins Browser-Tab, sowie eine konfigurierbare Stale-Zeit, nach der Daten im Hintergrund automatisch aktualisiert werden.
Auch Retry-Logik mit exponentiellem Backoff bei fehlgeschlagenen Requests, optimistische Updates bei Mutationen und die Deduplizierung gleichzeitiger identischer Anfragen sind Funktionen, die man zwar selbst nachbauen könnte, deren korrekte Implementierung aber überraschend viele Randfälle mit sich bringt. Genau an diesem Punkt beginnt der Aufwand, den Eigenbau zu pflegen, den Nutzen zu übersteigen, den er ursprünglich gegenüber einer fertigen Bibliothek hatte.
6. Wann TanStack Query klar überlegen ist
Sobald eine Anwendung mehrere Ansichten hat, die dieselben Daten aus unterschiedlichen Komponenten heraus anzeigen, etwa eine Produktliste und ein Warenkorb-Widget, die beide denselben Produktendpunkt abfragen, wird ein geteilter Cache zum entscheidenden Faktor. TanStack Query dedupliziert solche Anfragen automatisch, hält die Daten über die gesamte Anwendung synchron und invalidiert sie gezielt nach Mutationen, ohne dass man das selbst orchestrieren muss.
Auch bei Anforderungen wie Infinite Scroll, Pagination mit vorab geladenen nächsten Seiten, automatischem Retry mit Backoff oder Offline-Unterstützung ist der Umstieg auf TanStack Query fast immer die wirtschaftlichere Entscheidung gegenüber dem Ausbau des eigenen Hooks. Der eigene useApiResource-Hook bleibt trotzdem wertvoll, gerade für kleinere Projekte, Prototypen oder einzelne, isolierte Anfragen, bei denen die zusätzliche Abhängigkeit und ihre Lernkurve den Nutzen nicht rechtfertigen.
import { useQuery } from "@tanstack/react-query";
function ProductList() {
const { data, error, isLoading } = useQuery({
queryKey: ["products"],
queryFn: () => fetch("/api/products").then((res) => res.json()),
staleTime: 60_000,
});
if (isLoading) return <p>Lade Produkte...</p>;
if (error) return <p>Fehler: {error.message}</p>;
return (
<ul>
{data.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}
7. Custom Hooks isoliert testen
Ein zentraler Vorteil eines eigenen Hooks gegenüber verstreuter Fetch-Logik ist die Testbarkeit: useApiResource lässt sich mit Werkzeugen wie renderHook aus der React Testing Library isoliert testen, ganz ohne die Komponenten zu rendern, die ihn später verwenden. Ein gemocktes fetch reicht aus, um Loading-, Error- und Erfolgsfall gezielt durchzuspielen und dabei auch den cancelled-Schutz bei schnell wechselnden URLs zu verifizieren.
Diese Isolation zahlt sich besonders bei Refactorings aus: Ändert sich später etwa das Antwortformat der API oder kommt ein zusätzlicher Header hinzu, genügt eine Anpassung im Hook, begleitet von seinen eigenen Tests, statt jede einzelne Komponente erneut manuell zu überprüfen, die irgendwann einmal Daten von dieser Stelle geladen hat.
8. Typsicherheit mit generischen Hooks
In TypeScript-Projekten lohnt es sich, useApiResource generisch über den Datentyp zu machen, etwa useApiResource
Kombiniert mit einem Zod-Schema oder einer anderen Laufzeit-Validierung lässt sich sogar sicherstellen, dass die tatsächlich empfangenen JSON-Daten dem erwarteten Typ entsprechen, statt dem Server blind zu vertrauen. Das fängt Fälle ab, in denen sich ein Backend-Endpunkt ändert, ohne dass der Frontend-Typ synchron aktualisiert wurde, und verwandelt einen sonst erst zur Laufzeit sichtbaren Fehler in eine kontrollierte Fehlermeldung direkt im Hook.
function useApiResource<T>(url: string | null) {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
if (!url) {
setIsLoading(false);
return;
}
let cancelled = false;
(async () => {
setIsLoading(true);
setError(null);
try {
const response = await fetch(url);
const json = (await response.json()) as T;
if (!cancelled) setData(json);
} catch (err) {
if (!cancelled) setError(err as Error);
} finally {
if (!cancelled) setIsLoading(false);
}
})();
return () => {
cancelled = true;
};
}, [url]);
return { data, error, isLoading };
}
9. Die Entscheidung praktisch treffen
Für ein internes Admin-Tool mit wenigen, isolierten Ansichten, ein Prototyp, der schnell validiert werden muss, oder eine einzelne Komponente mit einer eigenständigen Anfrage ist ein schlanker useApiResource-Hook oft die richtige Wahl: keine zusätzliche Abhängigkeit, volle Kontrolle über das Verhalten, und die drei Kernprobleme Loading, Error und Race Conditions sind bereits sauber gelöst.
Sobald aber mehrere Komponenten dieselben Daten teilen, Nutzer regelmäßig zwischen Tabs wechseln und aktuelle Daten erwarten, oder Mutationen bestehende Ansichten synchron aktualisieren sollen, überwiegt der Aufwand, all das selbst nachzubauen, schnell den Aufwand, TanStack Query einzuführen und dessen Konzepte wie Query Keys und Invalidierung zu lernen. Die folgende Tabelle fasst diese Abwägung noch einmal konkret zusammen.
| Kriterium | Eigener useApiResource-Hook | TanStack Query | Empfehlung |
|---|---|---|---|
| Geteilter Cache über Komponenten | Nicht enthalten, muss selbst gebaut werden | Automatisch über Query Keys | Bei mehrfach genutzten Endpunkten TanStack Query |
| Zusätzliche Abhängigkeit | Keine, reiner React-Code | Eine weitere Bibliothek im Bundle | Bei kleinen Projekten Eigenbau bevorzugen |
| Retry mit Backoff, Offline-Support | Manuell nachzubauen | Standardmäßig konfigurierbar enthalten | Bei instabilen Netzwerken TanStack Query |
| Lernaufwand im Team | Gering, reines React-Wissen reicht | Zusätzliches Konzept: Query Keys, Invalidierung | Bei kleinen Teams zunächst Eigenbau erwägen |
Mironsoft
React-Architektur, Performance und Magento-Frontend-Integration
React-Frontends, die schnell bleiben statt mit jedem Feature langsamer zu werden?
Wir prüfen bestehende React-Anwendungen auf unnötige Re-Renders, aufgeblähte Bundles und fragile State-Verwaltung und bauen daraus ein Frontend, das performant bleibt und sich sauber an Magento oder andere Backends anbindet.
Performance-Audit
Re-Renders, Bundle-Größe und Ladezeiten systematisch messen und beheben.
State-Architektur
Context, Zustand und Server State sauber trennen statt alles in einen Topf zu werfen.
Magento-Integration
GraphQL- oder REST-Anbindung an Magento robust und typsicher aufbauen.
10. Zusammenfassung
Custom Hooks für Datenabruf: Das Wichtigste auf einen Blick
Kernidee
Loading-, Error- und Data-State plus Race-Condition-Schutz an einer Stelle bündeln statt in jeder Komponente neu.
Schutzmechanismus
Ein cancelled-Flag im useEffect-Cleanup verhindert, dass veraltete Antworten aktuellere Daten überschreiben.
Eigenbau-Grenze
Geteilter Cache, automatisches Refetch und Retry-Logik sind aufwendig selbst korrekt nachzubauen.
Umstieg auf Bibliothek
Sobald mehrere Komponenten dieselben Daten teilen, ist TanStack Query fast immer die wirtschaftlichere Wahl.