Zurück zum Blog
Okt. 2026·2 Min. LesezeitKI-generiert
Sprache:DeutschEnglishFrançaisÖsterreichischKlingonisch Originalfassung: English

Einen eigenen Data-Fetching-Hook in React bauen

#react#javascript#frontend

Jede React-Codebasis sammelt irgendwann ein Dutzend fast identischer useEffect-Blöcke an, die Daten laden, den Ladezustand verfolgen und Fehler behandeln. Dieses Muster in einen einzigen wiederverwendbaren Hook auszulagern spart nicht nur Tipparbeit – richtig gemacht behebt es auch ein paar subtile Bugs, die kopierte useEffect-Fetches fast immer haben.

Die naive Version und was daran falsch ist

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    setLoading(true);
    fetch(`/api/users/${userId}`)
      .then((res) => res.json())
      .then((data) => {
        setUser(data);
        setLoading(false);
      });
  }, [userId]);

  if (loading) return <p>Loading...</p>;
  return <h1>{user.name}</h1>;
}

Das sieht vernünftig aus, hat aber einen echten Bug: Ändert sich userId schnell (schnelle Navigation zwischen Profilen), sind mehrere Fetches gleichzeitig unterwegs, und es gibt keine Garantie, dass die zuletzt eintreffende Antwort zur aktuellen userId gehört. Man kann am Ende einen veralteten Nutzer rendern, weil eine ältere, langsamere Anfrage nach einer neueren, schnelleren fertig wurde.

Den Hook extrahieren, mit Abbruch

function useFetch(url) {
  const [state, setState] = useState({ data: null, loading: true, error: null });

  useEffect(() => {
    let cancelled = false;
    setState({ data: null, loading: true, error: null });

    fetch(url)
      .then((res) => {
        if (!res.ok) throw new Error(`Request failed: ${res.status}`);
        return res.json();
      })
      .then((data) => {
        if (!cancelled) setState({ data, loading: false, error: null });
      })
      .catch((error) => {
        if (!cancelled) setState({ data: null, loading: false, error });
      });

    return () => {
      cancelled = true;
    };
  }, [url]);

  return state;
}
function UserProfile({ userId }) {
  const { data: user, loading, error } = useFetch(`/api/users/${userId}`);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Something went wrong.</p>;
  return <h1>{user.name}</h1>;
}

Das cancelled-Flag ist die entscheidende Korrektur: Ändert sich url (oder wird die Komponente ausgehängt), läuft die Cleanup-Funktion des Effekts und setzt cancelled für den vorherigen Effektdurchlauf auf true. Löst sich dieser ältere Fetch später auf, prüft sein .then()-Callback das Flag und steigt aus, statt frischeren State mit veralteten Daten zu überschreiben.

AbortController statt eines booleschen Flags

Das cancelled-Flag hindert React daran, auf eine veraltete Antwort zu reagieren, aber die Netzwerkanfrage selbst läuft bis zum Ende weiter und verschwendet Bandbreite. AbortController bricht die laufende Anfrage tatsächlich ab:

function useFetch(url) {
  const [state, setState] = useState({ data: null, loading: true, error: null });

  useEffect(() => {
    const controller = new AbortController();
    setState({ data: null, loading: true, error: null });

    fetch(url, { signal: controller.signal })
      .then((res) => {
        if (!res.ok) throw new Error(`Request failed: ${res.status}`);
        return res.json();
      })
      .then((data) => setState({ data, loading: false, error: null }))
      .catch((error) => {
        if (error.name !== 'AbortError') {
          setState({ data: null, loading: false, error });
        }
      });

    return () => controller.abort();
  }, [url]);

  return state;
}

Die Prüfung auf error.name !== 'AbortError' ist wichtig: Das Abbrechen eines Fetch lässt sein Promise fehlschlagen, und ohne diese Prüfung würde jeder durch das Cleanup ausgelöste Abbruch fälschlich einen Fehlerzustand bei einer Komponente setzen, die längst weitergezogen ist.

Ein manuelles Refetch hinzufügen

Eine feste url-Abhängigkeit kümmert sich um automatisches Neuladen, wenn sich Props ändern, aber manchmal braucht man einen „Erneut versuchen"-Button, der dieselbe URL neu lädt. Dafür braucht es eine Möglichkeit, den Effekt anzustoßen, ohne url selbst zu ändern:

function useFetch(url) {
  const [state, setState] = useState({ data: null, loading: true, error: null });
  const [refetchIndex, setRefetchIndex] = useState(0);

  useEffect(() => {
    const controller = new AbortController();
    setState({ data: null, loading: true, error: null });

    fetch(url, { signal: controller.signal })
      .then((res) => {
        if (!res.ok) throw new Error(`Request failed: ${res.status}`);
        return res.json();
      })
      .then((data) => setState({ data, loading: false, error: null }))
      .catch((error) => {
        if (error.name !== 'AbortError') {
          setState({ data: null, loading: false, error });
        }
      });

    return () => controller.abort();
  }, [url, refetchIndex]);

  const refetch = () => setRefetchIndex((n) => n + 1);

  return { ...state, refetch };
}

refetchIndex hat keinen Einfluss auf die Anfrage selbst – es existiert allein als Abhängigkeit, auf die der Effekt reagieren kann, und gibt Aufrufern die Möglichkeit, einen erneuten Lauf zu erzwingen, ohne url zu ändern.

Was dieser Hook noch nicht kann

Diese Version ist ein solider Baustein, aber bewusst keine vollständige Data-Fetching-Lösung. Sie cacht keine Antworten über Komponenten hinweg, die dieselbe URL anfordern, dedupliziert keine gleichzeitigen Anfragen für dieselbe Ressource und wiederholt bei vorübergehenden Fehlern nichts. Genau diese Lücke füllen Bibliotheken wie React Query und SWR – sie bauen auf denselben useEffect- und Abbruch-Grundlagen auf, die hier gezeigt wurden, plus einem gemeinsamen Cache und einer Schicht zur Anfragen-Deduplizierung obendrauf. Eine eigene Version zu schreiben lohnt sich einmal, um zu verstehen, was tatsächlich passiert; sobald die App mehr als ein paar datenabhängige Komponenten hat, ist der Griff zu einer dieser Bibliotheken meist die richtige Entscheidung.

Fazit

Ein Data-Fetching-Hook ist im Kern eine Zustandsmaschine (idle → loading → success/error), eingewickelt in ein useEffect, das beim Cleanup sorgfältig ist. Die Abbruchlogik ist keine optionale Politur – ohne sie erzeugen schnelle Prop-Änderungen stillschweigend Race Conditions, bei denen eine alte Antwort eine neue überschreibt.

NeuerDer Trigramm-Algorithmus: Unscharfes Matching ohne MagieÄlterCSS Container Queries: Ein praktischer Leitfaden