Der React-useEffect-Hook verbindet deine Komponenten mit der Welt außerhalb von React — Netzwerkanfragen, Abonnements, Timer, Browser-APIs. Er ist mächtig, doch unachtsam eingesetzt erzeugt er Endlosschleifen, Speicherlecks und schwer zu debuggendes „Geister"-Verhalten. Dieser Leitfaden behandelt das Abhängigkeits-Array, die Cleanup-Logik und die häufigsten Fehler anhand echter Beispiele.
Wofür useEffect da ist
Ein Effekt ist ein Seiteneffekt, der nach dem Rendern läuft. React zeichnet zuerst den Bildschirm und führt dann deinen Effekt aus. Damit eignen sich Effekte für Arbeit, die mit der Außenwelt spricht und die Render-Ausgabe nicht direkt formt:
- Daten von einem Server abrufen (
fetch, axios) - Ein WebSocket oder Event-Abonnement einrichten
- Ein
setInterval/setTimeoutstarten - Den Dokumenttitel oder Browser-APIs aktualisieren
Wenn du nur einen Wert aus einem anderen berechnest, brauchst du keinen Effekt — tu das während des Renderns. Ein Effekt ist nötig, wenn du etwas außerhalb des Renderns berührst.
Das Abhängigkeits-Array: das Kernstück
useEffect verhält sich auf drei Arten, und der Unterschied liegt im zweiten Argument, dem Abhängigkeits-Array:
// 1) Läuft nach jedem Render
useEffect(() => {
console.log('jeder Render');
});
// 2) Läuft nur beim ersten Mount
useEffect(() => {
console.log('einmal');
}, []);
// 3) Läuft, sobald sich userId ändert
useEffect(() => {
fetchUser(userId);
}, [userId]);
Die Regel ist einfach: jeder reaktive Wert (Prop, State, Funktion), den du im Effekt verwendest, muss im Abhängigkeits-Array stehen. Eine Abhängigkeit auszulassen führt dazu, dass der Effekt mit veralteten (stale) Werten läuft. Das eslint-plugin-react-hooks des React-Teams liefert eine exhaustive-deps-Regel, die das für dich erkennt; behebe die Ursache, statt die Warnung stummzuschalten.
Die Cleanup-Funktion und Speicherlecks
Richtet ein Effekt etwas Dauerhaftes ein (ein Abonnement, einen Timer, einen Listener), bist du auch für dessen Abbau verantwortlich. Die Funktion, die du aus dem Effekt zurückgibst, läuft, bevor die Komponente aus dem DOM entfernt wird und bevor der Effekt das nächste Mal erneut läuft:
useEffect(() => {
const id = setInterval(() => {
setSeconds((s) => s + 1);
}, 1000);
return () => clearInterval(id); // Cleanup
}, []);
Ohne Cleanup stapelt jeder erneute Lauf ein neues setInterval auf, das selbst nach dem Unmount der Komponente weiterfeuert — ein klassisches Speicherleck. Dieselbe Regel gilt für Event-Listener:
useEffect(() => {
const onResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);
Race Conditions
Eine häufige Falle beim Abrufen: klickt sich der Nutzer schnell durch Datensätze, kann eine ältere Anfrage spät eintreffen und die neueren Daten überschreiben. Verhindere das mit einem Flag (oder einem AbortController) im Cleanup:
useEffect(() => {
let active = true;
fetch(`/api/users/${userId}`)
.then((r) => r.json())
.then((data) => {
if (active) setUser(data);
});
return () => {
active = false; // die veraltete Anfrage aktualisiert den State nicht mehr
};
}, [userId]);
In modernen Browsern ist es sauberer, die Anfrage mit AbortController tatsächlich abzubrechen. In größeren Apps übernehmen Bibliotheken wie TanStack Query oder SWR diese Race Conditions und das Caching für dich.
Die häufigsten Fehler
- Eine Abhängigkeit auslassen: Werte, die du aus dem Array weglässt, frieren ein (stale closure). Lösung: folge der
exhaustive-deps-Regel. - Endlosschleife mit leerem Array: das eigentliche Problem ist meist umgekehrt — ein Objekt/Array/eine Funktion, die bei jedem Render neu erzeugt wird, ins Array zu legen, löst den Effekt endlos erneut aus. Stabilisiere diese mit
useMemo/useCallback. - Abgeleitete Daten in einen Effekt legen: nutze keinen Effekt, um einen State aus einem anderen zu berechnen; berechne ihn während des Renderns.
- Cleanup vergessen: schreibe für jedes Abonnement/jeden Timer einen Abbau.
- Eine async-Funktion direkt übergeben:
useEffect(async () => …)ist falsch; der Effekt muss eine Cleanup-Funktion zurückgeben, kein Promise. Definiere und rufe innen eine separate async-Funktion auf.
Warum führt StrictMode den Effekt zweimal aus?
In der Entwicklung mountet React 18+ unter StrictMode jeden Effekt absichtlich einmal, unmountet ihn sofort und mountet ihn erneut. Ziel ist es aufzudecken, ob dein Cleanup korrekt geschrieben ist. Gerate nicht in Panik über einen „Bug", wenn dein Effekt zweimal feuert; schreibst du das Cleanup richtig, ist dieses Verhalten harmlos und tritt in einem Produktions-Build nicht auf.
Häufige Fragen
Was ist der Unterschied zwischen useEffect und useLayoutEffect?
useEffect läuft asynchron, nachdem der Browser das Zeichnen abgeschlossen hat; in den meisten Fällen ist das die richtige Wahl. useLayoutEffect läuft synchron nach der DOM-Aktualisierung, aber vor dem Zeichnen; nutze es nur, wenn du das Layout sofort messen und anpassen musst (etwa einen Tooltip positionieren), sonst kann es die Performance beeinträchtigen.
Was passiert, wenn ich nie ein Abhängigkeits-Array übergebe?
Lässt du das zweite Argument ganz weg, läuft der Effekt nach jedem Render. Das ist selten gewollt und bedeutet meist, dass du [] oder die echten Abhängigkeiten schreiben wolltest.
Ist ein Effekt der beste Weg, Daten abzurufen?
Für kleine Fälle ja, doch Bibliotheken wie TanStack Query oder SWR sind für die meisten Projekte die robustere Wahl, weil sie Caching, Wiederholungen und Race Conditions automatisch übernehmen.
Sind die Effekte in deiner React-Architektur außer Kontrolle geraten? Lass uns deine Komponenten durchgehen und stale closures sowie Lecks aufräumen. Nimm Kontakt auf und wir machen dein Projekt gemeinsam robust.