Custom Hooks für wiederverwendbare Datenabruf-Logik bauen
AI generated
{ }
React · Custom Hooks · Datenabruf
Custom Hooks für wiederverwendbare Datenabruf-Logik
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.

14 Min. Lesezeit Custom Hooks Datenabruf TanStack Query

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(url), damit data an der Aufrufstelle bereits korrekt typisiert ist, statt als unknown oder any behandelt zu werden. Das verhindert Tippfehler bei Feldnamen schon zur Compile-Zeit und macht Autovervollständigung in der IDE direkt nutzbar, ohne zusätzliche manuelle Type-Assertions an jeder Verwendungsstelle.

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.

11. FAQ: Custom Hooks für Datenabruf: Das Wichtigste auf einen Blick

1Was genau kapselt der useApiResource-Hook?
Er bündelt die drei Zustände data, error und isLoading zusammen mit einer refetch-Funktion in einem wiederverwendbaren Hook, sodass Komponenten nur noch diese vier Werte konsumieren, statt eigene useState- und useEffect-Kombinationen zu schreiben.
2Warum ist der cancelled-Flag im useEffect so wichtig?
Er verhindert, dass die Antwort eines veralteten Requests, etwa nach einem schnellen URL-Wechsel, den State mit überholten Daten überschreibt. Ohne diesen Schutz können Race Conditions dazu führen, dass die Oberfläche die falschen Daten anzeigt.
3Kann ich useApiResource auch für POST-Requests verwenden?
Grundsätzlich ja, sinnvoller ist aber ein separater useApiMutation-Hook, der die Anfrage erst bei expliziter Nutzeraktion über eine execute-Funktion auslöst, statt automatisch bei jedem Rendern der Komponente.
4Wie teste ich einen Custom Hook wie useApiResource isoliert?
Mit renderHook aus der React Testing Library lässt sich der Hook ohne umgebende Komponente ausführen. Ein gemocktes fetch erlaubt es, Loading-, Erfolgs- und Fehlerfall sowie das Verhalten bei schnell wechselnden URLs gezielt zu testen.
5Ab wann lohnt sich der Umstieg auf TanStack Query?
Sobald mehrere Komponenten dieselben Daten aus unterschiedlichen Endpunkten anzeigen, ein geteilter Cache gebraucht wird, oder Funktionen wie automatisches Refetch, Retry mit Backoff oder Pagination benötigt werden, übersteigt der Eigenbau-Aufwand meist den Nutzen.
6Verliere ich mit einem eigenen Hook Typsicherheit in TypeScript?
Nein, useApiResource lässt sich problemlos generisch über den erwarteten Datentyp gestalten, etwa useApiResource(url), sodass data an der Aufrufstelle korrekt typisiert ankommt, ohne any oder unknown.
7Was passiert, wenn zwei Komponenten gleichzeitig denselben useApiResource-Hook mit derselben URL nutzen?
Ohne geteilten Cache lösen beide Komponenten unabhängig voneinander einen eigenen Request aus, der Server wird also doppelt angefragt. Genau das ist einer der Hauptgründe, für solche Fälle auf TanStack Query mit seinem Query-Key-basierten Cache umzusteigen.
8Muss ich abhängige Abfragen mit verschachtelten useEffect-Aufrufen lösen?
Nein, es reicht, die URL des abhängigen Hooks bedingt auf null zu setzen, solange die Voraussetzung fehlt, etwa eine noch unbekannte Nutzer-ID. Der Hook überspringt den Fetch-Aufruf dann intern, ohne dass verschachtelte Effekte nötig sind.
9Ist ein selbstgebauter Hook schlechter als eine Bibliothek?
Nicht grundsätzlich, er löst die Kernprobleme Loading, Error und Race Conditions solide und bleibt für kleine Projekte oder isolierte Anfragen oft die einfachere Wahl. Erst bei geteiltem State über mehrere Komponenten oder komplexeren Anforderungen zieht eine Bibliothek klar davon.
10Kann ich später von useApiResource zu TanStack Query migrieren, ohne alles neu zu schreiben?
Ja, da beide dieselbe grundsätzliche Rückgabeform aus data, error und isLoading verwenden können, lässt sich die Migration Komponente für Komponente durchführen, indem der Hook-Import ausgetauscht und die Fetch-Funktion in eine queryFn überführt wird.