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

useEffect

Der Draht nach draußen – und der am meisten missbrauchte Hook

In einem Satz

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.

aufbau.tsx
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
Bildlich gesprochen

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

deps.tsx
useEffect(() => { … });          // nach JEDEM Zeichnen – fast immer falsch

useEffect(() => { … }, []);      // nur beim ersten Mal (und Aufräumen am Ende)

useEffect(() => { … }, [raumId]); // immer wenn raumId sich ändert
Achtung Die Liste ist ein Versprechen, kein Schalter

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.

cleanup.tsx
// 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]);
Gut zu wissen Warum läuft mein Effect zweimal?

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:

1. Einen Wert ausrechnen
zeichnet zweimal, kann veralten
const [vorname, setVorname] = useState("");
const [name, setName] = useState("");
const [voll, setVoll] = useState("");

useEffect(() => {
  setVoll(vorname + " " + name);
}, [vorname, name]);
einfach ausrechnen
const [vorname, setVorname] = useState("");
const [name, setName] = useState("");

const voll = vorname + " " + name;
2. Auf einen Klick reagieren
Umweg über den State
useEffect(() => {
  if (bestellt) {
    zeigeHinweis("Danke!");
  }
}, [bestellt]);
direkt im Klick-Handler
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.

3. Felder leeren, wenn ein anderer Datensatz kommt
zeigt kurz die alten Daten
useEffect(() => {
  setEntwurf("");
  setFehler(null);
}, [userId]);
React neu aufbauen lassen
// 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.

Nimm es, wenn …
  • +

    Timer: setInterval, setTimeout

  • +

    Auf Ereignisse horchen: addEventListener, Verbindungen, Beobachter

  • +

    Browser-Dinge anfassen: Seitentitel, localStorage, Video abspielen

  • +

    Fremde Bibliotheken einbinden (Karten, Diagramme, Editoren)

Lass es, wenn …
  • 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

läuft bei jedem Zeichnen erneut
const optionen = { url, versuche: 3 };   // jedes Mal ein NEUES Objekt

useEffect(() => {
  verbinde(optionen);
}, [optionen]);
läuft nur bei echter Änderung
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.

Stolperstein Warnung nie einfach abschalten

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

alte Antwort kann die neue überschreiben
useEffect(() => {
  fetch("/api/user/" + id)
    .then((r) => r.json())
    .then(setUser);
}, [id]);
veraltete Antwort verwerfen
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.

Tipp In echten Projekten nimmt man das selten selbst in die Hand

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

Stoppuhr mit sauberem Aufräumen

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>
  );
}

Den überflüssigen Effect wegwerfen

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 ap bleibt nur Apfel übrig
  • Die Datei enthält kein useEffect mehr
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>
  );
}

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 →