useEffect
Der Draht nach draußen – und der am meisten missbrauchte Hook
Mit useEffect sagst du React: „Wenn du fertig gezeichnet hast, mach bitte noch das hier“ – zum Beispiel einen Timer starten oder auf Tastendrücke horchen.
Deine Komponente darf beim Zeichnen nur rechnen und zurückgeben. Alles andere – einen Timer starten, auf das Tastatur-Ereignis horchen, den Seitentitel ändern – passiert danach, in einem Effect.
useEffect(() => {
// 1) Das hier läuft, NACHDEM React gezeichnet hat
return () => {
// 2) Das hier räumt wieder auf
};
}, [worauf, geachtet, wird]);
// ▲ die Liste am Ende bestimmt, WANN 1) erneut läuft Ein Effect ist kein „danach noch schnell etwas erledigen“. Er ist eine Dauer-Anweisung: „Solange diese Werte so sind, soll draußen dieser Zustand herrschen.“ Ändern sich die Werte, räumt React den alten Zustand ab und stellt den neuen her.
Die Liste am Ende: drei Fälle
useEffect(() => { … }); // nach JEDEM Zeichnen – fast immer falsch
useEffect(() => { … }, []); // nur beim ersten Mal (und Aufräumen am Ende)
useEffect(() => { … }, [raumId]); // immer wenn raumId sich ändert Du sagst React nicht „lauf bitte nur dann“. Du behauptest: „mehr als diese Werte benutze ich hier drin nicht“. Lässt du etwas weg, das du doch benutzt, arbeitet der Effect mit alten Daten. Deshalb: alles hinein, was von außen kommt.
Aufräumen ist Pflicht
Was du startest, musst du wieder beenden. Die Funktion, die du zurückgibst, ruft React auf, bevor der Effect erneut läuft – und ein letztes Mal, wenn die Komponente verschwindet.
// Timer
useEffect(() => {
const id = setInterval(() => setTick((t) => t + 1), 1000);
return () => clearInterval(id); // sonst laufen bald 10 Timer
}, []);
// Tastatur
useEffect(() => {
function onKey(e) { if (e.key === "Escape") schliessen(); }
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [schliessen]);
// Verbindung
useEffect(() => {
const verbindung = chat.verbinde(raumId);
return () => verbindung.trennen(); // beim Raumwechsel den alten trennen
}, [raumId]); Der StrictModeEin Prüfmodus, der während der Entwicklung absichtlich manches doppelt ausführt, um Fehler früh sichtbar zu machen.In der fertigen, veröffentlichten App passiert das nicht.→ macht in der Entwicklung absichtlich: starten → aufräumen → starten. Geht dabei etwas kaputt, fehlt dir das Aufräumen – und derselbe Fehler träte später beim Hin- und Herklicken auch wirklich auf. In der veröffentlichten App passiert das nicht.
Der wichtigste Teil: wann du KEINEN Effect brauchst
Die meisten Effects in echten Projekten sind überflüssig. Drei Muster, die du sofort erkennen kannst:
const [vorname, setVorname] = useState("");
const [name, setName] = useState("");
const [voll, setVoll] = useState("");
useEffect(() => {
setVoll(vorname + " " + name);
}, [vorname, name]); const [vorname, setVorname] = useState("");
const [name, setName] = useState("");
const voll = vorname + " " + name; useEffect(() => {
if (bestellt) {
zeigeHinweis("Danke!");
}
}, [bestellt]); function handleKauf() {
setBestellt(true);
zeigeHinweis("Danke!");
} Effects sind für Dinge, die passieren, weil die Komponente da ist – nicht, weil jemand geklickt hat. Was durch einen Klick ausgelöst wird, gehört in den Klick-Handler.
useEffect(() => {
setEntwurf("");
setFehler(null);
}, [userId]); // in der Komponente darüber:
<Profil key={userId} userId={userId} /> Ein neuer keyEin Namensschild an Listeneinträgen, damit React erkennt, welcher Eintrag welcher ist – auch wenn sich die Reihenfolge ändert.→ heißt für React: anderes Element. Es wirft das alte samt State weg und baut frisch auf.
- +
Timer:
setInterval,setTimeout - +
Auf Ereignisse horchen:
addEventListener, Verbindungen, Beobachter - +
Browser-Dinge anfassen: Seitentitel,
localStorage, Video abspielen - +
Fremde Bibliotheken einbinden (Karten, Diagramme, Editoren)
- −
Werte ausrechnen, die aus State oder Props folgen
- −
Auf Klicks reagieren – das kann der Handler
- −
State setzen, um damit den nächsten Effect auszulösen
- −
Daten laden in größeren Projekten – dafür gibt es fertige Bibliotheken
Die häufigste Endlosschleife
const optionen = { url, versuche: 3 }; // jedes Mal ein NEUES Objekt
useEffect(() => {
verbinde(optionen);
}, [optionen]); useEffect(() => {
const optionen = { url, versuche: 3 }; // im Effect erzeugt
verbinde(optionen);
}, [url]); Objekte, Arrays und Funktionen sind bei jedem Durchlauf neu – für React also „geändert“. In der Liste sollten möglichst nur einfache Werte stehen: Strings, Zahlen, Wahrheitswerte.
Wenn der LinterEin Werkzeug, das deinen Code beim Schreiben prüft und auf typische Fehler hinweist.→ eine fehlende Angabe in der Liste meldet, ist das fast immer ein echter Fehler. Die Lösung ist nie, die Warnung zu unterdrücken – sondern den Wert aufzunehmen, die Funktion in den Effect zu ziehen oder den Effect ganz loszuwerden.
▸ Genauer erklärt: Daten laden und das Wettrennen-Problem optional
Daten in einem Effect zu laden funktioniert – hat aber eine Tücke. Wechselt id schnell von 1 auf 2, laufen zwei Anfragen gleichzeitig. Welche zuerst zurückkommt, ist Zufall. Kommt die Antwort für 1 später an, überschreibt sie die für 2, und du zeigst den falschen Nutzer. Das nennt man Race ConditionZwei Vorgänge laufen gleichzeitig, und wer zuerst fertig wird, ist Zufall – mit unterschiedlichem Ergebnis je nach Ausgang.→.
useEffect(() => {
fetch("/api/user/" + id)
.then((r) => r.json())
.then(setUser);
}, [id]); useEffect(() => {
let aktuell = true;
fetch("/api/user/" + id)
.then((r) => r.json())
.then((daten) => {
if (aktuell) setUser(daten);
});
return () => { aktuell = false; };
}, [id]); Der Trick: Beim Aufräumen setzt du aktuell auf false. Die alte Anfrage läuft zwar zu Ende, ihr Ergebnis wird aber verworfen.
Fertige Bibliotheken wie TanStack Query oder SWR lösen Wettrennen, Zwischenspeichern, Wiederholversuche und Ladezustände auf einmal. Oder du lädst die Daten gleich auf dem Server – in Next.js ist das der Normalfall.
Sitzt das schon?
6 Fragen zu dieser Lektion. Falsche Antworten landen in deiner Statistik.
Jetzt selbst schreiben
Die Stoppuhr ignoriert den Start/Stopp-Knopf und bleibt bei 1 stehen. Beide Fehler haben mit dem Effect zu tun: Er achtet auf nichts und benutzt einen eingefrorenen Wert.
- □Solange „läuft“, erhöht sich die Anzeige jede Sekunde um 1
- □Nach Stopp bleibt sie stehen
- □Vor dem ersten Start passiert gar nichts
import { useEffect, useState } from "react"; export default function App() { const [seconds, setSeconds] = useState(0); const [running, setRunning] = useState(false); useEffect(() => { // TODO: Der Timer ignoriert "running" und rechnet immer mit der alten 0. const id = setInterval(() => setSeconds(seconds + 1), 1000); return () => clearInterval(id); }, []); return ( <div style={{ fontFamily: "system-ui", padding: 16 }}> <p data-testid="seconds" style={{ fontSize: 32 }}>{seconds}</p> <button onClick={() => setRunning((r) => !r)}> {running ? "Stopp" : "Start"} </button>{" "} <button onClick={() => setSeconds(0)}>Reset</button> </div> ); }
Diese Suche filtert mit einem Effect – dadurch zeichnet sie zweimal und zeigt beim ersten Mal kurz eine leere Liste. Räum das auf: ohne useEffect und ohne den zweiten State.
- □Ohne Eingabe erscheinen alle vier Früchte
- □Bei der Eingabe
apbleibt nurApfelübrig - □Die Datei enthält kein
useEffectmehr
import { useEffect, useState } from "react"; const FRUECHTE = ["Apfel", "Banane", "Kirsche", "Dattel"]; export default function App() { const [query, setQuery] = useState(""); const [sichtbar, setSichtbar] = useState<string[]>([]); // TODO: Diesen Effect ersatzlos streichen useEffect(() => { setSichtbar( FRUECHTE.filter((f) => f.toLowerCase().includes(query.toLowerCase())) ); }, [query]); return ( <div style={{ fontFamily: "system-ui", padding: 16 }}> <input aria-label="suche" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Suchen …" /> <ul> {sichtbar.map((f) => ( <li key={f}>{f}</li> ))} </ul> </div> ); }