Zum Inhalt
wannacode

Daten von einer API in einer Komponente

Kurz gesagt: Daten von einer API werden in einer React-Komponente aus useEffect geladen, mit dem Zustand „lädt“, „Fehler“ oder „fertig“, und ein AbortController im Aufräumen stoppt die veraltete Anfrage.

Problem

Äpfel unter „Getränke“

Der Katalog holt die Produkte der Kategorie von der API. Hier antwortet der Server für „Obst und Gemüse“ in 1,5 Sekunden und für die anderen Kategorien in 0,3.

React

Klick auf „Obst und Gemüse“ und gleich danach auf „Getränke“. Die Getränke erscheinen. Eine Sekunde später nehmen Äpfel, Bananen und Tomaten ihren Platz ein, während die Überschrift und der gedrückte Button weiter „Getränke“ sagen. Der Kunde sieht die falschen Produkte und weiß nicht, warum.

Die Idee

Eine Anfrage ist ein Effekt

Die Anfrage verlässt die Komponente in Richtung Server. Damit ist sie eine Nebenwirkung, und ihr Platz ist im Effekt, mit der Kategorie als Abhängigkeit: Jede neue Kategorie schickt eine neue Anfrage.

Die Zustände sind die aus der Lektion über fetch in der Seite: lädt, Fehler, fertig. Hier sind sie ein einziger Zustand, status, und die Komponente zeichnet den passenden Zweig, wie in der Lektion über bedingtes Zeichnen.

useEffect(() => {
async function load() {
setStatus('loading');
try {
const response = await fetch(`/api/products?category=${categoryId}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
setProducts(await response.json());
setStatus('ready');
} catch (e) {
setStatus('error');
}
}
load();
}, [categoryId]);

Die Funktion des Effekts darf nicht async sein: Sie muss das Aufräumen zurückgeben, und eine async-Funktion gibt ein Promise zurück. Deshalb ist load eine eigene async-Funktion im Effekt, die gleich aufgerufen wird.

status wird bei jedem Laden zu 'loading', nicht nur beim ersten Mal. Sonst bleibt bei einem Wechsel die alte Liste unter der neuen Überschrift stehen, bis die Antwort kommt.

Probier es aus

Lade die Kategorie

Catalog hat die Zustände products und status, aber nichts füllt sie. Lade die Produkte von categoryId von /api/products?category=… und zeige bei jedem Laden „Wird geladen…“. Die Prüfung schaut auf die Milchprodukte (5 Produkte), klickt dann auf „Getränke“ und wartet auf „Wird geladen…“ und 4 Produkte.

React

Die Idee

Der Wettlauf

Jeder Klick ändert die Kategorie und führt den Effekt noch einmal aus. Jeder Lauf schickt seine eigene Anfrage und wartet auf seine eigene Antwort. Keiner weiß von den anderen.

Antworten kommen nicht in der Reihenfolge der Anfragen. Der Server sucht das Obst vielleicht länger, das Netz bremst eine von beiden. Wenn der Kunde schnell wechselt, sind also zwei Anfragen gleichzeitig unterwegs. Die, die zuletzt antwortet, gewinnt, nicht die, die zuletzt losging.

Das heißt Wettlaufbedingung (Race Condition). Sie zeigt sich nicht, solange der Server schnell und gleichmäßig ist. Sie zeigt sich in einem langsamen Netz, auf einem Handy, bei einem ausgelasteten Server.

Sieh es dir an

Zwei Anfragen unterwegs

Die Balken wachsen, solange eine Anfrage unterwegs ist. Zuerst ohne Aufräumen, dann mit.

useEffect(() => {
const controller = new AbortController();
async function load() {
setStatus('loading');
try {
const url = `/api/products?category=${categoryId}`;
const response = await fetch(url, { signal: controller.signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
setProducts(await response.json());
setStatus('ready');
} catch (e) {
if (e.name === 'AbortError') return;
setStatus('error');
}
}
load();
return () => controller.abort();
}, [categoryId]);
Schritt für Schritt, in Worten
  1. Der Kunde klickt auf „Obst und Gemüse“. Die Kategorie ändert sich, der Effekt läuft und schickt eine Anfrage. Diese ist langsam.
  2. Bevor sie antwortet, klickt der Kunde auf „Getränke“. Der Effekt läuft wieder und schickt eine zweite Anfrage. Jetzt sind zwei Anfragen gleichzeitig unterwegs.
  3. Die schnelle Anfrage antwortet zuerst: Die Liste zeigt die Getränke.
  4. Angenommen, das Aufräumen fehlt. Die langsame Anfrage antwortet zuletzt, ihr Lauf setzt ebenfalls die Produkte, und unter „Getränke“ steht Obst.
  5. Jetzt mit dem Aufräumen. Jeder Lauf des Effekts erzeugt seinen eigenen AbortController und gibt dessen signal an fetch weiter.
  6. Die neue Kategorie ruft das Aufräumen des alten Laufs auf: abort() stoppt die langsame Anfrage. Ihr fetch wirft AbortError, und catch überspringt ihn: Das ist kein Fehler.
  7. Nur die Anfrage für die Getränke antwortet. Liste und Überschrift passen zusammen.
  8. Die Regel: Eine Anfrage in einem Effekt hat im Aufräumen einen Abbruch. Der letzte Lauf gewinnt, weil alle davor gestoppt wurden.

Ohne Aufräumen zeigt der Bildschirm die letzte Antwort. Mit Aufräumen wird die alte Anfrage gestoppt, bevor sie antwortet, und die letzte Antwort gehört immer zur letzten Kategorie.

Die Idee

Abbrechen im Aufräumen

Der Effekt hat schon einen Platz für „stopp das Alte“: das Aufräumen. Der Browser hat ein Werkzeug, um eine Anfrage zu stoppen: AbortController.

  1. Jeder Lauf des Effekts erzeugt seinen eigenen controller = new AbortController().
  2. Er gibt sein controller.signal an fetch weiter: fetch(url, { signal: controller.signal }).
  3. Das Aufräumen ruft controller.abort() auf. Die Anfrage stoppt, und await fetch(…) wirft einen Fehler namens AbortError.
  4. catch überspringt diesen Fehler: if (e.name === 'AbortError') return;. Die Anfrage ist nicht fehlgeschlagen; der Code hat sie gestoppt.
useEffect(() => {
const controller = new AbortController();
async function load() {
setStatus('loading');
try {
const response = await fetch(url, { signal: controller.signal });
…
} catch (e) {
if (e.name === 'AbortError') return;
setStatus('error');
}
}
load();
return () => controller.abort();
}, [categoryId]);

Das Aufräumen läuft auch, wenn die Komponente verschwindet. Der Kunde hat den Katalog verlassen: Die Anfrage stoppt, und niemand setzt Produkte in einer Komponente, die es nicht mehr gibt.

Probier es aus

Stoppe die alte Anfrage

Sorge dafür, dass die alte Anfrage stoppt, wenn der Kunde schnell wechselt. Die Prüfung klickt auf „Obst und Gemüse“ und gleich danach auf „Getränke“, wartet auf „Wird geladen…“ und die Getränke, wartet die langsame Antwort ab und stellt sicher, dass keine Äpfel dastehen und keine Fehlermeldung erschienen ist.

React

Probier es aus

Finde den Fehler

Der Kunde wechselt schnell und sieht kurz „Die Produkte wurden nicht geladen.“, dann kommen die Produkte. Der Server hat keinen Fehler zurückgegeben. Welche Zeile ist schuld?

Wähle die Zeile mit dem Fehler.

Probier es aus

Fülle die Lücke

Was stoppt die alte Anfrage?

useEffect(() => {
  const controller = new AbortController();
  fetch(url, { signal: controller.signal }).then(show);
  return () => controller.();
}, [categoryId]);

Die Idee

Fehler und „Noch einmal versuchen“

Manchmal antwortet der Server mit 500. Die Prüfung von response.ok wirft, catch setzt 'error', und die Komponente zeigt die Meldung mit role="alert". Der Kunde soll es noch einmal versuchen können, ohne die Seite neu zu laden.

Noch einmal versuchen heißt dasselbe Laden. Das macht der Effekt schon, wenn sich eine Abhängigkeit ändert. Also ändert „Noch einmal versuchen“ eine Abhängigkeit: eine Zahl attempt, die nur die Versuche zählt.

const [attempt, setAttempt] = useState(0);
useEffect(() => {
// … dasselbe Laden
}, [categoryId, attempt]);
<button type="button" onClick={() => setAttempt(attempt + 1)}>Noch einmal versuchen</button>

Der neue Lauf setzt 'loading', deshalb verschwinden die Meldung und der Button, solange die Anfrage unterwegs ist.

Genauer, für Neugierige

Daten-Bibliotheken. In einem echten Projekt wird das Laden selten in jeder Komponente von Hand geschrieben. Es gibt Bibliotheken, die das und mehr erledigen: Sie halten die Antworten in einem Cache, schicken nicht zwei gleiche Anfragen, versuchen es von selbst noch einmal und aktualisieren die Daten, wenn der Kunde zum Tab zurückkommt. Für React sind das meist TanStack Query und SWR. Sie sind nicht in der Sandbox, und der Kurs hat keine Lektionen dazu. Im Inneren tun sie dasselbe wie hier: eine Anfrage in einem Effekt, Zustände, ein Abbruch.

Ein Flag statt eines Abbruchs. Statt eines AbortController kann das Aufräumen ein Flag setzen: let ignore = false; im Effekt, ignore = true im Aufräumen und if (!ignore) vor dem Setzen der Produkte. Die alte Anfrage erreicht den Server trotzdem, aber ihre Antwort wird verworfen. Ein Abbruch stoppt die Anfrage selbst. Beide Wege lösen den Wettlauf.

StrictMode. In der Entwicklung mit <StrictMode> läuft der Effekt, wird aufgeräumt und läuft noch einmal. Deshalb zeigt das Netzwerk zwei Anfragen, die erste abgebrochen. Das ist der Test, dass das Aufräumen funktioniert.

Übung

Noch einmal versuchen

Hier bekommt die erste Anfrage für die Milchprodukte eine 500. Der Katalog zeigt „Die Produkte wurden nicht geladen.“, aber es gibt keine Möglichkeit, es noch einmal zu versuchen. Füge einen Button „Noch einmal versuchen“ hinzu, der das Laden erneut ausführt. Der Abbruch beim schnellen Wechseln muss bleiben. Die Prüfung wartet auf den Fehler, klickt auf „Noch einmal versuchen“, wartet auf „Wird geladen…“ und 5 Produkte und wechselt dann schnell zu Obst und Getränken.

React

Test

Teste dich

1. Der Kunde klickt auf „Obst und Gemüse“, dann auf „Getränke“. Die Antworten kommen in umgekehrter Reihenfolge. Was zeigt Code ohne Aufräumen?
2. Warum steht die Anfrage in einer async function load() im Effekt und nicht in useEffect(async () => …)?
3. Warum überspringt catch den AbortError, statt „Die Produkte wurden nicht geladen.“ anzuzeigen?
4. Wie schickt „Noch einmal versuchen“ die Anfrage erneut?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·