Stolpersteine: Effects & Closures
Endlosschleifen, alte Werte, doppelte Timer
Fast jeder Effect-Fehler hat eine von vier Ursachen: falsche Liste am Ende, fehlendes Aufräumen, ein Effect der gar keiner sein sollte, oder eine Funktion mit veralteten Werten.
useEffect ist der Hook, an dem die meiste Zeit verloren geht. Die gute Nachricht: Es sind immer wieder dieselben vier Ursachen.
1. Die Endlosschleife
// a) keine Dependency-Liste + setState
useEffect(() => {
setDaten(rechne(x));
}); // Render -> Effect -> Render -> …
// b) Objekt/Array als Dependency
const filter = { aktiv: true };
useEffect(() => { lade(filter); }, [filter]); // filter ist immer neu
// c) Funktion aus dem Elternteil als Dependency
useEffect(() => { onLoad(); }, [onLoad]); // onLoad ist inline definiert Schreib ein console.log in den Effect und gib die Dependencies mit aus. Sieh dir an, welche sich ändert. In 90 % der Fälle ist es ein Objekt, ein Array oder eine Funktion, die bei jedem Render neu entsteht.
function Eltern() {
const laden = () => fetch("/api"); // neu bei jedem Render
return <Kind laden={laden} />;
}
function Kind({ laden }) {
useEffect(() => { laden(); }, [laden]);
} function Eltern() {
const laden = useCallback(() => fetch("/api"), []);
return <Kind laden={laden} />;
}
// Oder besser: die Funktion ganz in den Effect ziehen,
// wenn nur das Kind sie braucht. 2. Effect-Ketten
useEffect(() => {
setGefiltert(items.filter(passt));
}, [items]);
useEffect(() => {
setSortiert([...gefiltert].sort(cmp));
}, [gefiltert]);
useEffect(() => {
setSeite(sortiert.slice(0, 20));
}, [sortiert]); const seite = useMemo(() => {
return items.filter(passt).sort(cmp).slice(0, 20);
}, [items]); Jeder Effect, der nur State aus State macht, kostet einen zusätzlichen Render und zeigt zwischendurch inkonsistente Daten. Rechne stattdessen direkt.
3. Vergessenes Aufräumen
Ohne Cleanup passiert typischerweise:
setInterval -> nach 10 Wechseln laufen 10 Timer
addEventListener -> Handler feuern mehrfach, Speicher leckt
WebSocket / SSE -> Verbindungen bleiben offen
IntersectionObserver -> beobachtet Knoten, die es nicht mehr gibt
fetch -> setState auf einer ausgehängten Komponente Läuft dein Effect in der Entwicklung zweimal und es entsteht ein Doppel-Effekt (zwei Verbindungen, doppelte Zeilen), fehlt das Aufräumen. Der StrictMode ist hier der Bote – nicht der Täter.
4. Der Effect, der keiner sein sollte
// ❌ Reaktion auf einen Klick
useEffect(() => { if (gesendet) toast("Danke!"); }, [gesendet]);
// ✅ gehört in den Handler
// ❌ State zurücksetzen, wenn eine Prop wechselt
useEffect(() => { setEntwurf(""); }, [userId]);
// ✅ <Editor key={userId} />
// ❌ Eltern informieren
useEffect(() => { onChange(wert); }, [wert]);
// ✅ onChange direkt dort aufrufen, wo sich wert ändert
// ❌ Ableiten
useEffect(() => { setVoll(vor + " " + nach); }, [vor, nach]);
// ✅ const voll = vor + " " + nach; 5. Funktionen, die alte Werte festhalten
Jeder Durchlauf erzeugt neue Funktionen – mit den Werten von genau diesem Durchlauf. Bleibt eine alte Funktion in einem Timer oder Listener hängen, arbeitet sie für immer mit den damaligen Werten weiter. Der Fachbegriff dafür ist Stale ClosureEine Funktion arbeitet mit veralteten Werten, weil sie in einem früheren Durchlauf entstanden ist und die alten Werte festhält.→.
function Suche({ onSuchen }) {
const [text, setText] = useState("");
useEffect(() => {
const id = setTimeout(() => onSuchen(text), 500); // text von JETZT
return () => clearTimeout(id);
}, [text, onSuchen]); // beide Werte gehören hinein
}
// Gefährlich wird es, wenn jemand die Liste "aufräumt" und [] daraus macht:
// Dann ruft der Timer für immer onSuchen("") auf – mit dem text des
// allerersten Renders. // eslint-disable-next-line react-hooks/exhaustive-deps ist fast immer ein verstecktes Bug-Ticket. Die legitimen Ausnahmen sind so selten, dass sie einen Kommentar mit Begründung verdienen.
Für den Fall „ich will den aktuellen Wert lesen, aber nicht darauf reagieren“ gibt es einen kommenden Hook (useEffectEvent, derzeit experimentell). Bis dahin: Wert in eine Ref spiegeln oder den Effect umbauen.
Sitzt das schon?
5 Fragen zu dieser Lektion. Falsche Antworten landen in deiner Statistik.
Jetzt selbst schreiben
Drei Effects reichen State an State weiter. Das Ergebnis stimmt zwar irgendwann, aber es kostet drei zusätzliche Renders und flackert. Ersetze die Kette durch eine direkte Berechnung.
- □Die Anzeige zeigt die drei teuersten Artikel, absteigend sortiert
- □Die Datei enthält kein
useEffectmehr - □Es gibt nur noch einen State: die Rohdaten
import { useEffect, useState } from "react"; type Artikel = { name: string; preis: number; aktiv: boolean }; const ARTIKEL: Artikel[] = [ { name: "Stuhl", preis: 80, aktiv: true }, { name: "Tisch", preis: 250, aktiv: true }, { name: "Lampe", preis: 40, aktiv: false }, { name: "Regal", preis: 120, aktiv: true }, { name: "Sofa", preis: 900, aktiv: true }, ]; export default function App() { const [artikel] = useState(ARTIKEL); const [aktive, setAktive] = useState<Artikel[]>([]); const [sortiert, setSortiert] = useState<Artikel[]>([]); const [top, setTop] = useState<Artikel[]>([]); useEffect(() => { setAktive(artikel.filter((a) => a.aktiv)); }, [artikel]); useEffect(() => { setSortiert([...aktive].sort((a, b) => b.preis - a.preis)); }, [aktive]); useEffect(() => { setTop(sortiert.slice(0, 3)); }, [sortiert]); return ( <div style={{ fontFamily: "system-ui", padding: 16 }}> <h3>Teuerste aktive Artikel</h3> <ol> {top.map((a) => ( <li key={a.name}> {a.name}: {a.preis} € </li> ))} </ol> </div> ); }