Startseite / Artikel

Prüfung von useEffect: Zehn häufige Fehler, ihre Symptome und echte Lösungen

Erkennen Sie die zehn häufigsten Fehler bei useEffect in React – von fehlender Bereinigung und veralteten Schließungen über Race Conditions bis hin zur Synchronisierung von Props – und wenden Sie die richtige Lösung an.

Verlag: REACTAPP.TOP8 Min. Lesezeit1919 Wörter

Quellen: 10 React useEffect Mistakes That Are Secretly Breaking Your App in 2026

React 19, der React Compiler sowie Server Components haben viel Arbeit aus useEffect herausverlagert, doch dieser Hook ist weiterhin der Ort, an dem viele der schwierigsten React-Bugs auftreten: Datenlecks, veraltete Daten, Flackern sowie Render-Schleifen. Die meisten entstehen durch zehn wiederkehrende Fehler. Unten wird für jeden Fehler die fehlerhafte Version, das Symptom sowie eine Lösung aufgeführt, gefolgt von einer Checkliste zur Überprüfung eigener Effects.

Das Prinzip hinter jeder Lösung: useEffect dient dazu, einen Component mit etwas außerhalb von React zu synchronisieren – beispielsweise mit einem Timer, einer Socket-Verbindung oder einer Browser-API.

1. Die Cleanup-Funktion vergessen

Timer, Abonnements und Event-Listener, die in einem Effect erstellt werden, laufen weiter, nachdem sich der Component neu rendern oder abgemontet wurde – es sei denn, man deaktiviert sie explizit. Hier wird der Zeitintervall niemals beendet:

// ❌ Broken: No cleanup
useEffect(() => {
  const timer = setInterval(() => {
    setCount(c => c + 1);
  }, 1000);
  // Missing: return () => clearInterval(timer);
}, []);

Die Symptome sind undichte Intervalle sowie doppelte Zuhörer. Während der Entwicklung aktiviert Strict Mode absichtlich das Einbinden, Entbinden und Neu-Einbinden von Komponenten, um dies sichtbar zu machen: Ohne Aufräumarbeiten entstehen zwei Intervalle und der Zähler erhöht sich zweimal pro Sekunde. In der Produktion tritt das Leck immer dann auf, wenn die Komponente neu eingebunden wird. Geben Sie eine Funktion zurück, die die Einrichtung rückgängig macht:

// ✅ Fixed: Always clean up
useEffect(() => {
  const timer = setInterval(() => {
    setCount(c => c + 1);
  }, 1000);
  return () => clearInterval(timer);
}, []);

2. Verwendung eines leeren Abhängigkeitsarrays zur Angabe von „einmal ausführen“

Ein leeres Array bedeutet in keiner besonderen Weise „beim Einbinden“. Es teilt React mit, dass der Effekt von nichts abhängt, weshalb er niemals erneut ausgeführt wird. Wenn der Effekt Props oder State liest, bleiben diese Werte bei ihren Werten nach der ersten Darstellung unverändert:

// ❌ Broken: Stale closure
function ChatRoom({ roomId }) {
  useEffect(() => {
    connectToRoom(roomId);
    return () => disconnectFromRoom(roomId);
  }, []); // roomId changes, but effect never re-runs
}

Wenn sich roomId ändert, bleibt das Komponente im ursprünglichen Raum. Listen Sie alle reaktiven Werte auf, die der Effect verwendet; die Lint-Regel react-hooks/exhaustive-deps erkennt dies.

// ✅ Fixed: Include real dependencies
useEffect(() => {
  connectToRoom(roomId);
  return () => disconnectFromRoom(roomId);
}, [roomId]);

Jetzt trennt React sich jedes Mal, wenn sich roomId ändert, vom vorherigen Raum und verbindet sich mit dem neuen.

3. Abhängigkeit von Objekten, die bei jeder Renderung neu erstellt werden

Abhängigkeiten werden per Referenz verglichen. Ein Objekt oder ein Array-Literal, das während der Renderung erstellt wird, hat jedes Mal eine neue Referenz – daher wird ein Effect, der davon abhängt, nach jeder einzelnen Renderung erneut ausgeführt:

// ❌ Broken: New object every render
function Parent() {
  const config = { timeout: 5000 }; // New reference every time

  useEffect(() => {
    fetchData(config);
  }, [config]); // Runs every single render
}

Stabilisieren Sie die Referenz. Wenn der Wert sich niemals ändert, definieren Sie ihn einmal außerhalb des Components. Wenn er aus Props oder State gebildet wird, umhüllen Sie ihn mit useMemo zusammen mit den richtigen Abhängigkeiten. Der folgende Auszug zeigt beide Optionen nebeneinander; in der Praxis würden Sie eine davon wählen:

// ✅ Fixed: Stable reference
const CONFIG = { timeout: 5000 }; // Defined outside component
function Parent() {
  const config = useMemo(() => ({ timeout: 5000 }), []);

  useEffect(() => {
    fetchData(config);
  }, [config]);
}

Oft noch einfacher: Verlassen Sie sich auf Primitive (timeout anstelle von config) und bauen Sie das Objekt innerhalb des Effects auf. Der React Compiler merkt sich viele Werte im kompilierten Code, aber nicht Objekte, die aus unkomprimiertem Code oder Bibliotheken stammen, daher sollten Sie sich nicht darauf verlassen, dass die Effects korrekt funktionieren.

4. Ermöglichen von gleichzeitigen asynchronen Antworten

Wenn sich eine Abhängigkeit schnell ändert – wie beispielsweise bei einer Suchleiste oder schnellem Tabwechsel – können gleichzeitig mehrere Anfragen ausgesendet werden, die nicht unbedingt in der richtigen Reihenfolge abgeschlossen werden:

// ❌ Broken: Race condition
useEffect(() => {
  fetch(`/api/search?q=${query}`).then(res => res.json()).then(setResults);
}, [query]);

Falls ein Benutzer zuerst „react“ und anschließend „react hooks“ eingibt, kann die Antwort für „react“ zuletzt eintreffen und die korrekten Ergebnisse überschreiben. Die Lösung besteht darin, den veralteten Anfragevorgang in der Cleanup-Funktion abzubrechen. AbortController beendet den Fetch-Vorgang, und der entstehende AbortError wird ignoriert, sodass er nicht als echter Fehler gemeldet wird:

// ✅ Fixed: Abort in-flight requests
useEffect(() => {
  const controller = new AbortController();

  fetch(`/api/search?q=${query}`, { signal: controller.signal })
    .then(res => res.json())
    .then(setResults)
    .catch(err => {
      if (err.name !== 'AbortError') setError(err);
    });

  return () => controller.abort();
}, [query]);

Insbesondere bei Suchoberflächen gibt es weitere Feinheiten, die in der Behebung von Rennbedingungen, die durch Debouncing nicht gelöst werden können behandelt werden.

5. Speichern von abgeleiteten Werten im State

Falls ein Wert aus vorhandenen Props oder State berechnet werden kann, benötigt er keinen eigenen State oder ein Effect:

// ❌ Broken: Redundant state + effect
const [fullName, setFullName] = useState('');
useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

Jede Änderung wird zweimal dargestellt – zunächst mit einem veralteten fullName – wodurch es nun zwei Quellen der Wahrheit gibt. Berechnen Sie den Wert während des Renderings und verwenden Sie useMemo nur für wirklich aufwändige Berechnungen:

// ✅ Fixed: Compute during render
const fullName = `${firstName} ${lastName}`;// Or for expensive computations
const fullName = useMemo(() => `${firstName} ${lastName}`, [firstName, lastName]);

Beachten Sie, dass der Codeausschnitt zwei Alternativen zeigt; die mehrfache Deklaration von fullName im selben Scope ist ein Syntaxfehler, daher sollte nur eine Variante verwendet werden. Das allgemeine Muster wird in einer sichereren Alternative zur Synchronisierung von State mit useEffect erläutert.

6. Erstellen eines Update-Laufs

Ein Effect, der State setzt, hängt auch dauerhaft von seiner eigenen Ausführung ab:

// ❌ Broken: Infinite loop
useEffect(() => {
  setCount(count + 1);
}, [count]);

Die Aktualisierung löst eine Neuzeichnung aus, die Neuzeichnung ändert count, der geänderte count führt den Effekt erneut aus und so weiter. React stoppt dies schließlich mit dem Fehler „Maximum update depth exceeded“. Eine funktionale Aktualisierung beseitigt die Notwendigkeit, count innerhalb des Effekts zu lesen, sodass er nicht mehr als Abhängigkeit erforderlich ist:

// ✅ Fixed: Functional update + correct dependencies
useEffect(() => {
  const timer = setTimeout(() => {
    setCount(c => c + 1); // Functional update avoids dependency
  }, 1000);
  return () => clearTimeout(timer);
}, []); // No count dependency needed

Beachten Sie, dass dies das Verhalten ändert: Der Zähler erhöht sich nun erst eine Sekunde nach dem Einbinden um eins. Für einen tickenden Zähler sollten Sie setInterval mit einer Bereinigungsfunktion wie in Fehler 1 verwenden. Jeder setState innerhalb eines Effekts benötigt eine Bedingung, die sicherstellt, dass er abgeschlossen wird.

7. Die Abhängigkeitsliste vollständig weglassen

Ohne zweites Argument wird ein Effekt nach jeder Neuzeichnung ausgeführt. Das ist manchmal gewünscht, aber in der Regel ein Zufall und aufwändig:

// ❌ Broken: Runs on every render
useEffect(() => {
  console.log('Effect running');
  performHeavyComputation(data);
});

Der React Compiler reduziert unnötige Neuladungen, macht die Ausführung der Codeblöcke in Ihren Effects jedoch nicht effizienter. Daher bleibt ein unüberwachter Effect, der aufwendige Aufgaben ausführt, weiterhin ein Engpass. Deklarieren Sie die tatsächlichen Abhängigkeiten:

// ✅ Fixed: Explicit dependencies
useEffect(() => {
  performHeavyComputation(data);
}, [data]);

8. Handhabung von Benutzerereignissen in Effects

Effects werden ausgeführt, weil etwas gerendert wird – nicht, weil ein Benutzer etwas getan hat. Analysen, die an einem Zustandsflagge gebunden sind, werden jedes Mal ausgelöst, wenn sich diese Flagge aus irgendeinem Grund ändert:

// ❌ Broken: Event logic in effect
useEffect(() => {
  if (isModalOpen) {
    trackEvent('modal_opened');
  }
}, [isModalOpen]);

Dieses Ereignis wird beim Initialisieren protokolliert, wenn das Modalfenster bereits geöffnet ist, wenn ein Elternelement die Eigenschaft ändert oder wenn der Zustand wiederhergestellt wird – und nicht nur, wenn ein Benutzer klickt. Legen Sie die Interaktionslogik in den Handler, der die Interaktion ausführt:

// ✅ Fixed: Event handler
function handleOpenModal() {
  setIsModalOpen(true);
  trackEvent('modal_opened');
}

Falls Sie die Benutzeraktion benennen können, die den Code auslösen soll, gehört sie in einen Handler.

9. Datenerfassung mit einem einfachen Effect

Da Server-Komponenten sowie Framework-Loader in vielen Anwendungen die Initialdaten verarbeiten, ist das Abrufen von Daten auf der Client-Seite in Effects seltener notwendig. Wenn man dennoch auf der Client-Seite Daten abruft, ist ein einfaches Effect anfällig:

// ❌ Broken: Raw fetch in effect
useEffect(() => {
  fetch('/api/user').then(r => r.json()).then(setUser);
}, []);

Es verfügt weder über einen Ladezustand noch über Fehlerbehandlung, keine Deduplizierung von Anfragen, kein Caching und – wie Fehler 4 zeigt – auch keinen Schutz vor Rennbedingungen. Eine Datenabruf-Bibliothek kümmert sich um all das:

// ✅ Fixed: Use a data-fetching library
const { data: user, isLoading, error } = useQuery({
  queryKey: ['user'],
  queryFn: fetchUser,
  staleTime: 5 * 60 * 1000,
});

TanStack Query und SWR sind die üblichen Wahlmöglichkeiten. Ohne sie sollte man stattdessen einen eigenen Custom-Hook mit Handling für Ladezustände, Fehler und Abbrüche schreiben, anstatt ad-hoc Effects für den Datenabruf zu verwenden.

10. Kopieren von Props in den State und Synchronisierung derselben

Eine Komponente, die einen Initialwert erhält, diesen in den State kopiert und anschließend ein Effect verwendet, um Änderungen an den Props nachzubilden, arbeitet gegen sich selbst:

// ❌ Broken: Prop-to-state sync
function EmailInput({ initialEmail }) {
  const [email, setEmail] = useState(initialEmail);

  useEffect(() => {
    setEmail(initialEmail); // Sync prop to state
  }, [initialEmail]);

  return <input value={email} onChange={e => setEmail(e.target.value)} />;
}

Jedes Mal, wenn der Elternteil einen neuen initialEmail bereitstellt, wird das Eingetippte des Benutzers nach einer zusätzlichen Darstellung mit dem alten Wert stillschweigend überschrieben. Wählen Sie einen Ansatz: Machen Sie das Komponenten vollständig kontrolliert oder lassen Sie es unkontrolliert und setzen Sie es absichtlich durch Änderung seiner key zurück – dadurch erhält React eine neue Instanz mit neuem Zustand:

// ✅ Fixed: Controlled or fully uncontrolled
// Option 1: Fully controlled — no internal state
function EmailInput({ email, onChange }) {
  return <input value={email} onChange={onChange} />;
}
// Option 2: Fully uncontrolled with key
function EmailInput({ initialEmail }) {
  const [email, setEmail] = useState(initialEmail);
  return <input value={email} onChange={e => setEmail(e.target.value)} />;
}
// Usage: Reset by changing key
<EmailInput key={userId} initialEmail={user.email} />

Wie React 19 die Situation verändert

Neue Versionen von React machen diese Fehler sowohl sichtbarer als auch weniger notwendig:

  • Durch den ausschließlich für die Entwicklung vorgesehenen doppelten Einbindungsmechanismus von Effekten in Strict Mode werden fehlende oder falsche Aufräumvorgänge frühzeitig aufgedeckt.
  • Der React Compiler merkt Komponenten sowie viele Werte automatisch ab, was versehentliche erneute Darstellungen verringert, aber die Logik der Effekte nicht korrigiert.
  • Serverkomponenten übernehmen den größten Teil des initialen Datenzuladens und lassen die Effekte hauptsächlich für clientseitige Aufgaben wie Abonnements, Animationen und Browser-APIs übrig.
  • Die use-API kann während des Renderns Promises und Kontexte lesen, wodurch einige Gründe für den Einsatz von Effekten bei asynchronen Werten entfallen.
  • Prülliste vor dem Zusammenführen von Effekten

    Vor der Veröffentlichung eines Effekts prüfen Sie:

    • Jedes Abonnement, Timer und Listener verfügt über eine entsprechende Aufräumfunktion.
    • Der Abhängigkeitsarray listet alle reaktiven Werte auf, die der Effekt liest.
    • Objektabhängigkeiten und Array-Abhängigkeiten sind stabil oder werden durch primitive Abhängigkeiten ersetzt.
    • Asynchrone Aufgaben werden bei Änderungen der Abhängigkeiten abgebrochen oder ignoriert.
    • Nichts, was im Effekt berechnet wird, könnte nicht auch während des Renderns ermittelt werden.
    • Keine Zustandsaktualisierung kann denselben Effekt ohne Stoppbedingung erneut auslösen.
    • Die vom Benutzer ausgelöste Logik befindet sich in den Event-Handlern.
    • Die Datenerfassung erfolgt möglichst über eine Serverkomponente, einen Framework-Loader oder eine Bibliothek zur Datenerfassung.
    • Jeder Zustandswert hat genau eine einzige Quelle der Wahrheit.

    Zusammenfassung

    Fast jeder Fehler mit useEffect entsteht dadurch, dass der Hook für etwas anderes als externe Synchronisierung verwendet wird. Abgeleitete Werte gehören in die Render-Funktion, Benutzeraktionen in die Handler, Serverdaten in eine Datenschicht und wiederherstellbarer Zustand hinter einem key. Die verbleibenden echten Abonnements benötigen genaue Abhängigkeiten sowie eine ordnungsgemäße Bereinigung. Oft ist das beste Ergebnis einer Überprüfung die Löschung eines solchen Effects.