Effekte
Kurz gesagt: Ein Effekt ist Code für Nebenwirkungen (ein Timer, eine Anfrage, der Titel des Tabs). useEffect führt ihn nach dem Zeichnen aus und räumt vor dem nächsten Lauf und beim Verschwinden der Komponente auf.
Problem
Das Banner ist weg, die Prüfungen nicht
Die Banitsa mit Käse ist ausverkauft. Der Kunde klickt auf „Bestand beobachten“, und unter dem Button erscheint ein Banner. Solange das Banner offen ist, prüft die Seite jede Sekunde den Bestand. Hier schreibt die Prüfung nur eine Zeile in die Konsole.
Konsole
Klick auf „Bestand beobachten“, warte zwei Sekunden und klick auf „Beobachtung beenden“. Das Banner verschwindet, doch „Prüfung Nr. 3“, „Prüfung Nr. 4“ … kommen weiter in der Konsole an. Öffne und schließe das Banner noch einmal: Jetzt sind es zwei Zeilen pro Sekunde. Der Timer gehört dem Browser, nicht der Komponente. Niemand hat ihn gestoppt.
Die Idee
Das Zeichnen berechnet nur
Eine Komponente sagt, was auf dem Bildschirm stehen soll: Aus Props und Zustand berechnet sie das Markup. Das ist das Zeichnen. Die Bibliothek wiederholt es, wann sie will, und vergleicht das Ergebnis mit dem vorherigen.
Deshalb muss das Zeichnen rein sein: Es tut nichts außer das Markup zu berechnen. Ein Timer, eine Anfrage an den Server, eine Änderung von document.title, ein Abonnement von Ereignissen auf window tun etwas nach außen. Das sind Nebenwirkungen.
Der Rumpf der Funktion StockWatcher ist das Zeichnen. Er läuft bei jedem Zeichnen, und im Beispiel oben startet er jedes Mal einen neuen Timer. Man sieht es hier nicht, weil das Banner nur einmal gezeichnet wird. Wenn der Timer aber den Zustand ändert (etwa einen Zähler der Prüfungen auf dem Bildschirm), zeichnet jede Änderung neu, und jedes Zeichnen startet einen weiteren Timer. Ihre Zahl wächst mit jeder Sekunde.
Eine Nebenwirkung braucht zwei Dinge, die das Zeichnen nicht liefert: einen Moment zum Starten und einen Moment zum Stoppen.
Die Idee
Effekt und Aufräumen
Ein Effekt ist Code, den die Komponente ausführt, sobald sie auf der Seite ist. Das Aufräumen ist der Code, der stoppt, was gestartet wurde, wenn die Komponente verschwindet.
import { useEffect } from 'react';
export default function StockWatcher({ product }) { useEffect(() => { const id = setInterval(check, 1000); return () => clearInterval(id); }, [product]);
return <p role="status">Wir beobachten den Bestand von {product.name}.</p>;}useEffect(Funktion, Abhängigkeiten)sagt React: „Führe diese Funktion aus, sobald die Änderungen im DOM sind.“- Die Funktion, die der Effekt zurückgibt, ist das Aufräumen. React ruft sie auf, wenn die Komponente die Seite verlässt.
- Das zweite Argument ist die Liste der Abhängigkeiten: die Werte, die der Effekt liest. Die nächste Idee handelt davon.
Das Aufräumen spiegelt den Effekt: setInterval und clearInterval, addEventListener und removeEventListener, eine Anfrage und das Abbrechen dieser Anfrage.
Probier es aus
Stoppe die Prüfungen
Der Timer in StockWatcher startet direkt in der Komponente, und niemand stoppt ihn. Verschiebe ihn in einen Effekt, der startet, wenn das Banner erscheint, und stoppe ihn, wenn das Banner verschwindet. Die Prüfung öffnet das Banner, wartet auf „Prüfung Nr. 1“, schließt es und stellt sicher, dass „Prüfung Nr. 2“ nie kommt.
Konsole
Lösung
App.jsximport { useState } from 'react';
import StockWatcher from './StockWatcher.jsx';
const PRODUCT = { id: 4, name: 'Banitsa mit Käse' };
export default function App() {
const [watching, setWatching] = useState(false);
return (
<main>
<h1>{PRODUCT.name}</h1>
<p>Ausverkauft. Bald wieder da.</p>
<button type="button" onClick={() => setWatching(!watching)}>
{watching ? 'Beobachtung beenden' : 'Bestand beobachten'}
</button>
{watching && <StockWatcher product={PRODUCT} />}
</main>
);
}
StockWatcher.jsximport { useEffect } from 'react';
export default function StockWatcher({ product }) {
useEffect(() => {
let checks = 0;
const id = setInterval(() => {
checks++;
console.log(`Prüfung Nr. ${checks}: ${product.name}`);
}, 1000);
return () => clearInterval(id);
}, [product]);
return (
<p className="watcher" role="status">
Wir beobachten den Bestand von {product.name}. Wir sagen dir Bescheid, sobald er wieder da ist.
</p>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 0 16px; }
.watcher { background: #f3efe6; border-left: 4px solid #c27c2a; padding: 8px 12px; }
label { display: block; margin: 8px 0; }
button, select { font: inherit; }
Der Timer gehört in useEffect. Der Effekt gibt () => clearInterval(id) zurück, und die Abhängigkeiten sind [product]. Die Prüfung erkennt einen Effekt ohne return: Der Timer bleibt trotzdem. Sie erkennt auch return clearInterval(id); ohne den Pfeil: Dann wird clearInterval sofort aufgerufen, der Timer stoppt, bevor er auch nur einmal geprüft hat, und React bekommt undefined statt einer Funktion.
Die Idee
Abhängigkeiten
Jetzt kann der Kunde ein anderes ausverkauftes Produkt wählen, während das Banner offen ist. Der Effekt prüft product. Sobald sich das Produkt ändert, muss die alte Prüfung stoppen und eine neue für das neue Produkt starten.
Dafür ist die Liste der Abhängigkeiten da. Nach jedem Zeichnen vergleicht React jeden Wert darin mit dem vom letzten Mal. Ist mindestens einer anders, ruft es zuerst das Aufräumen des alten Laufs auf und führt dann den Effekt noch einmal aus.
| Das zweite Argument | Wann der Effekt läuft |
|---|---|
[product] |
nach dem ersten Zeichnen und nach jedem Zeichnen, bei dem product anders ist |
[] |
nur nach dem ersten Zeichnen |
| keines | nach jedem Zeichnen |
Die Regel: Die Liste enthält jeden Wert aus Props und Zustand, den der Effekt liest. Fehlt product, bleibt der Effekt beim alten Produkt. Die Funktion des Effekts wurde beim ersten Zeichnen erstellt und merkt sich die Werte von damals.
Sieh es dir an
Das Leben eines Effekts
Das Banner öffnet sich für die Banitsa, der Kunde wählt die Gurken, dann schließt er das Banner.
function StockWatcher({ product }) { useEffect(() => { let checks = 0; const id = setInterval(() => { checks++; console.log(`Prüfung Nr. ${checks}: ${product.name}`); }, 1000); return () => clearInterval(id); }, [product]);
return <p role="status">Wir beobachten den Bestand von {product.name}</p>;}Schritt für Schritt, in Worten
- Zeichnen: React ruft StockWatcher auf und bekommt das Markup. Die Funktion berechnet nur, was angezeigt wird. Sie startet nichts.
- Die Komponente ist auf der Seite. Jetzt führt React den Effekt aus: Der Timer startet und zählt die Prüfungen.
- Der Kunde wählt ein anderes Produkt. Die Prop ändert sich, StockWatcher wird neu gezeichnet, und React vergleicht [product] mit dem letzten Mal: Es ist anders.
- Vor dem neuen Lauf kommt das Aufräumen des alten: Sein Timer stoppt. Ohne das würden die alten Prüfungen neben den neuen weiterlaufen.
- Der Effekt läuft noch einmal mit dem neuen Produkt. Sein eigener Timer zählt ab Nr. 1.
- Der Kunde schließt das Banner: Die Komponente verlässt die Seite. Das letzte Aufräumen läuft, und der Timer stoppt damit.
- Jeder Lauf hat sein Gegenstück: Was der Effekt startet, stoppt sein Aufräumen. Vor dem nächsten Lauf und wenn die Komponente geht.
Der Effekt läuft zweimal, und das Aufräumen läuft zweimal: einmal vor dem neuen Lauf, einmal, wenn die Komponente entfernt wird. Dazwischen gibt es immer genau einen Timer.
Probier es aus
Finde den Fehler
Wähle die Zeile mit dem Fehler.
[] heißt „nur nach dem ersten Zeichnen“. Der Effekt läuft für das neue Produkt nicht noch einmal, und seine Funktion merkt sich das alte. Es muss [product] heißen.Probier es aus
Fülle die Lücke
Was gehört in die Lücke, damit der Timer stoppt, wenn die Komponente verschwindet?
useEffect(() => {
const id = setInterval(check, 1000);
() => clearInterval(id);
}, [product]);return: Die Funktion, die der Effekt zurückgibt, ist das Aufräumen. React ruft sie vor dem nächsten Lauf und beim Entfernen der Komponente auf.Die Idee
Wann du keinen Effekt brauchst
Ein Effekt ist für eine Verbindung zu etwas außerhalb der Komponente: ein Timer, ein Server, der Titel des Tabs, window. Zwei Dinge sehen nach einem Effekt aus, sind aber keiner.
- Ein Wert, der aus anderen berechnet wird. Die Summe des Warenkorbs steht nicht in einem eigenen Zustand, den ein Effekt aktualisiert. Sie wird beim Zeichnen berechnet (oder mit
useMemo), wie in der Lektion über abgeleiteten Zustand. Ein Effekt, der sie in einen eigenen Zustand schreibt, ist unnötige Arbeit und eine zweite Stelle, die vom Warenkorb abweichen kann. - Eine Reaktion auf eine Aktion. Die Bestellung wird gesendet, weil der Kunde auf „Bestellen“ geklickt hat. Das gehört in den Click-Handler, nicht in einen Effekt, der ein Flag beobachtet.
Was bleibt, ist die eigentliche Aufgabe eines Effekts: die Komponente mit der Außenwelt synchron zu halten, solange sie auf der Seite ist.
Genauer, für Neugierige
Der doppelte Lauf in der Entwicklung. In einem neuen Vite-Projekt packt main.jsx <App /> in <StrictMode>. In der Entwicklung führt StrictMode jeden Effekt absichtlich aus, räumt ihn auf und führt ihn noch einmal aus, gleich beim Anzeigen. Wenn die Konsole zweimal „der Effekt hat gestartet“ zeigt, ist das ein Test, kein Fehler: Ein Effekt mit korrektem Aufräumen übersteht ihn. Die Sandbox benutzt StrictMode nicht, deshalb startet der Effekt hier einmal.
Warum nicht useEffect(async () => …). Eine async-Funktion gibt ein Promise zurück, und React erwartet eine Aufräumfunktion oder nichts. Die Konsole sagt useEffect must not return anything besides a function, which is used for clean-up. Die async-Funktion schreibt man im Effekt und ruft sie dort auf. Das kommt in der nächsten Lektion.
Wie die Abhängigkeiten verglichen werden. Mit Object.is, wie beim Zustand. Ein Objekt, das bei jedem Zeichnen neu erstellt wird, ist jedes Mal anders, und der Effekt läuft jedes Mal. Die Regel „alles, was der Effekt liest, steht in der Liste“ bewacht der Linter (react-hooks/exhaustive-deps), der zur Vite-Vorlage gehört.
Klassenkomponenten haben dasselbe in drei Methoden: componentDidMount, componentDidUpdate und componentWillUnmount.
Übung
Ein anderes Produkt
Über dem Banner gibt es ein Dropdown „Produkt“ mit der Banitsa und den Gurken. Im Moment startet StockWatcher den Timer einmal, wenn er angezeigt wird. Sorge dafür, dass beim Wechsel des Produkts die alte Prüfung stoppt und eine neue ab Nr. 1 für das neue Produkt startet. Die Prüfung öffnet das Banner, wartet auf „Prüfung Nr. 1“ für die Banitsa, wählt die Gurken, wartet auf „Prüfung Nr. 1“ für sie, stellt sicher, dass „Prüfung Nr. 2“ für die Banitsa nie kommt, und schließt das Banner.
Konsole
Lösung
App.jsximport { useState } from 'react';
import StockWatcher from './StockWatcher.jsx';
const PRODUCTS = [
{ id: 4, name: 'Banitsa mit Käse' },
{ id: 13, name: 'Gurken' },
];
export default function App() {
const [productId, setProductId] = useState(4);
const [watching, setWatching] = useState(false);
const product = PRODUCTS.find((p) => p.id === productId);
return (
<main>
<label>
Produkt{' '}
<select value={productId} onChange={(e) => setProductId(Number(e.target.value))}>
{PRODUCTS.map((p) => (
<option key={p.id} value={p.id}>{p.name}</option>
))}
</select>
</label>
<h1>{product.name}</h1>
<p>Ausverkauft. Bald wieder da.</p>
<button type="button" onClick={() => setWatching(!watching)}>
{watching ? 'Beobachtung beenden' : 'Bestand beobachten'}
</button>
{watching && <StockWatcher product={product} />}
</main>
);
}
StockWatcher.jsximport { useEffect } from 'react';
export default function StockWatcher({ product }) {
useEffect(() => {
let checks = 0;
const id = setInterval(() => {
checks++;
console.log(`Prüfung Nr. ${checks}: ${product.name}`);
}, 1000);
return () => clearInterval(id);
}, [product]);
return (
<p className="watcher" role="status">
Wir beobachten den Bestand von {product.name}. Wir sagen dir Bescheid, sobald er wieder da ist.
</p>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 0 16px; }
.watcher { background: #f3efe6; border-left: 4px solid #c27c2a; padding: 8px 12px; }
label { display: block; margin: 8px 0; }
button, select { font: inherit; }
Die Liste der Abhängigkeiten wird [product], und der Effekt gibt () => clearInterval(id) zurück. Bei einer Änderung ruft React zuerst das Aufräumen des alten Timers auf und führt dann den Effekt mit dem neuen Produkt aus. Die Prüfung erkennt [product] ohne Aufräumen: Dann laufen beide Timer nebeneinander.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Props: Daten von oben · Bedingungen: leer, Fehler, lädt · Zustand · Abgeleiteter Zustand
- Geht weiter in: Daten von einer API in einer Komponente · Geschwindigkeit: wann eine Komponente neu rendert · Komponenten testen · Von der Sandbox zum Projekt
- Aufgaben zum Thema: Effekte, Daten und Formulare