Hirefullstack – Software Engineering & IT-Beratung aus Berlin
← Zur Übersicht
Kern 15 Min Lesezeit

Stolpersteine: Effects & Closures

Endlosschleifen, alte Werte, doppelte Timer

Baut auf: useEffect
In einem Satz

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

endlos.tsx
// 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
Tipp So findest du die Ursache

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.

Funktion als Dependency entschärfen
So nicht
function Eltern() {
  const laden = () => fetch("/api");    // neu bei jedem Render
  return <Kind laden={laden} />;
}

function Kind({ laden }) {
  useEffect(() => { laden(); }, [laden]);
}
So geht's
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

vier Renders, schwer zu lesen
useEffect(() => {
  setGefiltert(items.filter(passt));
}, [items]);

useEffect(() => {
  setSortiert([...gefiltert].sort(cmp));
}, [gefiltert]);

useEffect(() => {
  setSeite(sortiert.slice(0, 20));
}, [sortiert]);
eine Berechnung
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

cleanup.txt
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
Stolperstein Der StrictMode zeigt es dir

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

kein-effect.tsx
// ❌ 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..

closure.tsx
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.
Achtung Nie die Lint-Warnung abschalten

// 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.

Gut zu wissen Ausblick: useEffectEvent

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

Effect-Kette auflösen

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 useEffect mehr
  • 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>
  );
}

Hirefullstack

Ihr braucht React-Verstärkung im Team?

Wir bauen seit Jahren React- und Next.js-Anwendungen für Kunden in ganz Deutschland – als einzelner Experte, als Verstärkung fürs Bestandsteam oder als komplettes Scrum-Team.

Projekt besprechen →