Um in React Daten zu laden, startest du die Anfrage in useEffect, speicherst die Antwort im State und renderst eine Ladeanzeige, bis sie ankommt. Setze die Werte, von denen die Anfrage abhängt, etwa eine ID, ins Abhängigkeitsarray, damit der Effekt erneut lädt, wenn sie sich ändern.
Die Beispiele auf dieser Seite können das Netzwerk nicht erreichen, also ist fetchUser eine Fake-API: ein Promise, das nach einer Verzögerung aufgelöst wird, so wie fetch. Ändere 800 zu 3000, damit der Ladetext länger stehen bleibt.
Mit einer echten API sieht der Körper des Effekts so aus:
useEffect(() => {
fetch('https://api.example.com/users/1')
.then((res) => res.json())
.then((data) => setUser(data));
}, []);
Laden, Fehler und Daten
Eine Anfrage hat drei Ergebnisse, die der Bildschirm zeigen sollte: lädt noch, fehlgeschlagen oder fertig. Halte jedes davon im State und setze sie zurück, wenn der Effekt eine neue Anfrage startet.
Klicke auf User 3: Die Fake-API lehnt ab, die Fehlermeldung ersetzt den Namen, und die Konsole zeigt den Fehler. Der Block finally setzt loading auf beiden Wegen zurück, also lässt eine fehlgeschlagene Anfrage den Spinner nie stehen.
Wenn fetch mit einem echten Server spricht, lehnt eine Antwort mit 404 oder 500 das Promise nicht ab. Prüfe res.ok und wirf den Fehler selbst:
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
async/await in einem Effekt
Die Funktion, die du an useEffect übergibst, kann nicht async sein. React erwartet, dass sie nichts oder eine Cleanup-Funktion zurückgibt, und eine async-Funktion gibt immer ein Promise zurück.
// Wrong: the effect returns a promise
useEffect(async () => {
const data = await fetchUser(id);
setUser(data);
}, [id]);
// Right: define an async function inside and call it
useEffect(() => {
async function load() {
const data = await fetchUser(id);
setUser(data);
}
load();
}, [id]);
Race Conditions
Wenn sich die ID schnell ändert, sind zwei Anfragen gleichzeitig unterwegs. Nichts garantiert, dass sie in der richtigen Reihenfolge antworten. Wenn die ältere langsamer ist, kommt sie zuletzt an und überschreibt die Daten für die ID, die zuletzt gewählt wurde.
Die Lösung ist ein Flag ignore. Jeder Durchlauf des Effekts hat sein eigenes Flag, und sein Cleanup setzt es auf true. Weil React das Cleanup vor dem nächsten Durchlauf ausführt, sieht eine Antwort aus einem veralteten Durchlauf ignore === true und wird verworfen. Dieser Block rendert dasselbe Profil zweimal, ohne und mit Flag:
Klicke auf den Button und warte. Die schnelle Antwort für User 2 kommt zuerst, also loggen beide Profile und behalten Grace. Etwa anderthalb Sekunden nach dem Klick kommt die langsame Antwort für User 1: Das Profil ohne Flag wechselt zu Ada, obwohl User 2 gewählt ist, während das mit Flag dropped stale Ada loggt und Grace behält.
Derselbe Bug taucht in Suchfeldern, Tabs und jeder Liste auf, die vom Server gefiltert wird. Das Ladebeispiel oben lässt das Flag weg, um kurz zu bleiben, aber jedes Laden in einem Effekt sollte es haben, oder den Abbruch unten.
Abbrechen mit AbortController
Das Flag ignore verwirft eine veraltete Antwort, aber die Anfrage läuft trotzdem bis zum Ende. Mit dem echten fetch kannst du sie abbrechen. Erzeuge im Effekt einen AbortController, übergib sein signal an fetch und ruf im Cleanup abort() auf:
useEffect(() => {
const controller = new AbortController();
async function load() {
try {
const res = await fetch(`/api/users/${id}`, { signal: controller.signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
setUser(await res.json());
} catch (err) {
if (err.name === 'AbortError') return; // cancelled on purpose
setError(err);
}
}
load();
return () => controller.abort();
}, [id]);
Ein abgebrochenes fetch lehnt mit einem AbortError ab, also überspringt der catch-Block ihn, statt ihn als Fehlschlag anzuzeigen. Der Abbruch deckt auch das Unmounten ab: Wenn die Komponente entfernt wird, bricht ihr Cleanup die Anfrage ab.
In einen Custom Hook verschieben
Ladezustand, Fehler und der Schutz vor Races sind in jeder Komponente, die Daten lädt, gleich. Pack sie in einen Custom Hook, und jede Komponente fragt Daten in einer Zeile an.
fetchPosts ist außerhalb der Komponente definiert, also ändert sich seine Identität nie, und es ist im Abhängigkeitsarray sicher. In App definiert wäre es bei jedem Render eine neue Funktion, der Effekt liefe nach jedem Render, und weil der Effekt State setzt, würde er nie aufhören zu laden.
Ändere 500 zu 2000 und klicke zweimal hintereinander auf den Button: Die Konsole zeigt zwei Anfragen, und nur die Posts für das Thema, bei dem du gelandet bist, erscheinen.
Laden in einem Event-Handler
Ein Effekt ist für Daten, die die Komponente braucht, weil sie auf dem Bildschirm ist: Eine Profilseite lädt ihr Profil. Wenn eine Anfrage passiert, weil jemand etwas getan hat, etwa auf Suchen oder Speichern gedrückt, stelle sie im Event-Handler. Du weißt genau, was sie ausgelöst hat, und nichts läuft erneut, wenn sich unabhängiger State ändert.
async function handleSubmit(e) {
e.preventDefault();
setStatus('saving');
const res = await fetch('/api/notes', { method: 'POST', body: JSON.stringify({ text }) });
setStatus(res.ok ? 'saved' : 'error');
}
Typische Fehler
Fehlende Abhängigkeiten. Ein Effekt, der id liest, aber [] hat, lädt für immer den ersten Nutzer. Führe jeden Wert auf, den die Anfrage nutzt.
Im Komponentenkörper laden. Ein fetch außerhalb eines Effekts läuft bei jedem Render, und wenn es State setzt, startet es eine Schleife.
Darauf vertrauen, dass fetch bei Fehlern ablehnt. Es lehnt nur ab, wenn das Netzwerk versagt. Prüfe res.ok.
Das Zurücksetzen des Ladezustands vergessen. Wenn sich die ID ändert, setze loading wieder auf true, sonst bleiben die alten Daten auf dem Bildschirm, ohne Hinweis, dass neue unterwegs sind.
Wann eine Bibliothek oder ein Framework
Laden in einem Effekt reicht für ein paar Anfragen. Es cacht nicht: Öffne dasselbe Profil zweimal, und es lädt zweimal. Es teilt keine Daten zwischen Komponenten, wiederholt bei Fehlschlägen nicht und lädt nicht neu, wenn der Tab wieder den Fokus bekommt. Bibliotheken erledigen das für dich:
import { useQuery } from '@tanstack/react-query';
function Profile({ id }) {
const { data, error, isPending } = useQuery({
queryKey: ['user', id],
queryFn: () => fetch(`/api/users/${id}`).then((res) => res.json()),
});
if (isPending) return <p>Loading...</p>;
if (error) return <p>{error.message}</p>;
return <p>{data.name}</p>;
}
TanStack Query und SWR sind die üblichen Optionen für clientseitige Daten. Frameworks gehen weiter und laden Daten auf dem Server, bevor die Seite den Browser erreicht: Next.js mit Server Components, React Router mit Loadern. Das vermeidet das Aufblitzen der Ladeanzeige und die Kette von Anfragen, die entsteht, wenn die Daten eines Elternteils ankommen müssen, bevor ein Kind überhaupt mit dem Laden beginnen kann.
React 19: use() mit Suspense
React 19 bringt use, das beim Rendern ein Promise liest. Die Komponente suspendiert, bis das Promise aufgelöst ist, und die nächste <Suspense>-Grenze zeigt in der Zwischenzeit einen Fallback, also hat die Komponente selbst keinen Ladezustand:
import { use, Suspense } from 'react';
function Profile({ userPromise }) {
const user = use(userPromise);
return <p>{user.name}</p>;
}
<Suspense fallback={<p>Loading...</p>}>
<Profile userPromise={userPromise} />
</Suspense>
Das Promise muss außerhalb der Komponente erzeugt werden (von einem Framework, einem Cache oder einem Elternteil), nicht in dem Render, der es liest, sonst startet jeder Render eine neue Anfrage. Die Seite zum use-Hook behandelt das mit ausführbaren Beispielen.
Häufig gestellte Fragen
Wie lade ich Daten, wenn eine React-Komponente lädt?
Starte die Anfrage in einem useEffect mit den Werten, von denen sie abhängt, im Abhängigkeitsarray ([], wenn es keine gibt), und speichere das Ergebnis mit useState im State. Rendere eine Ladeanzeige, bis die Daten da sind.
Warum kann der useEffect-Callback nicht async sein?
Eine async-Funktion gibt immer ein Promise zurück, und React erwartet, dass der Effekt entweder nichts oder eine Cleanup-Funktion zurückgibt. Schreib im Effekt eine async-Funktion und ruf sie sofort auf.
Was ist eine Race Condition beim Laden von Daten in React?
Wenn sich die Eingabe schnell ändert, sind zwei Anfragen unterwegs, und die ältere kann zuletzt antworten und die neueren Daten überschreiben. Setze im Cleanup des Effekts ein Flag ignore und überspring setState, wenn es gesetzt ist, oder brich die Anfrage mit einem AbortController ab.
Sollte ich useEffect oder eine Bibliothek zum Laden von Daten nutzen?
useEffect funktioniert für kleine Apps und lohnt sich zu verstehen. Für Caching, Deduplizierung, Wiederholungen und erneutes Laden erspart dir eine Bibliothek wie TanStack Query oder SWR oder das in ein Framework wie Next.js eingebaute Laden von Daten, diese Logik selbst zu schreiben.
Wie zeige ich einen Lade-Spinner, während Daten laden?
Halte einen Boolean loading (oder einen String status) im State, setze ihn vor dem Start der Anfrage und setze ihn zurück, wenn die Anfrage fertig ist, sowohl im Erfolgs- als auch im Fehlerfall. Rendere den Spinner, solange er true ist.