Warum Ihr useEffect zweimal ausgeführt wird
Erfahren Sie, warum React StrictMode useEffect in der Entwicklung zweimal ausführt und wie Sie Abrufe, Listener und andere Effekte mit Cleanup beheben.
Im Entwicklungsmodus führt React StrictMode bei jedem Effect zuerst das Setup aus, dann das Cleanup und anschließend erneut das Setup. Jeder Effect, der Daten abruft, ein Abonnement anlegt oder etwas protokolliert, wird dadurch sichtbar zweimal ausgeführt.
Sie schreiben also einen einzigen fetch-Aufruf in einen Effect, öffnen den Network-Tab und sehen zwei identische Requests und zwei Konsolenausgaben. Das sieht nach einem Bug in Ihrem Code oder in React aus.
Es ist weder das eine noch das andere. Die doppelte Ausführung ist ein bewusster Test Ihrer Cleanup-Logik. Dieser Artikel erklärt, was StrictMode dabei tut, wie Sie die drei häufigsten Effects korrigieren, die diesen Test nicht bestehen, welche „Fixes“ das Problem nur verschleiern und woran Sie erkennen, dass Ihr Effect korrekt ist. Falls Sie Ihr Wissen über den Hook selbst auffrischen möchten, beginnen Sie mit diesem Leitfaden zu Reacts useEffect-Hook.
Das Wichtigste in Kürze
- StrictMode führt für jeden Effect einen zusätzlichen Setup- und Cleanup-Durchlauf aus, und zwar nur im Entwicklungsmodus, sowohl in React 18 als auch in React 19.
- StrictMode ist optional, aber das React-Template von Vite umschließt die App mit
<StrictMode>, und der Next.js App Router aktiviert StrictMode standardmäßig. - Eine korrekte Cleanup-Funktion macht genau das rückgängig, was das Setup getan hat: Sie bricht den Request ab, entfernt den Listener, löscht den Timer oder schließt die Verbindung.
- Zwei Requests im Network-Tab sind im Entwicklungsmodus auch nach einem korrekten Fix zu erwarten. Entscheidend ist, dass nur die jeweils neueste Response den State aktualisieren kann.
- Wer StrictMode entfernt oder einen
useRef-Guard nach dem Muster „wurde bereits ausgeführt“ einbaut, verschleiert fehlendes Cleanup, ohne es zu beheben.
Das Symptom: Ein Fetch, der zweimal ausgelöst wird
Ein Effect zum Datenabruf ohne Cleanup ist der häufigste Weg, auf dem Entwickler dieses Verhalten entdecken. Die folgende Komponente funktioniert in Production einwandfrei, feuert im Entwicklungsmodus jedoch zweimal:
import { useState, useEffect } from 'react';
function ProductDetails({ id }) {
const [product, setProduct] = useState(null);
useEffect(() => {
console.log('setup');
fetch(`/api/products/${id}`)
.then((res) => res.json())
.then((data) => setProduct(data));
}, [id]);
return <h2>{product?.name}</h2>;
}
Sie erhalten zwei setup-Logs und zwei Requests. React erfährt an keiner Stelle, wie der erste Request abgebrochen werden soll, also laufen beide bis zum Ende durch.
Warum wird useEffect im Entwicklungsmodus zweimal ausgeführt?
Die React-Referenz zu StrictMode erklärt, dass React bei aktiviertem StrictMode im Entwicklungsmodus jedem Effect einen zusätzlichen Setup- und Cleanup-Durchlauf hinzufügt. Ihr Effect führt also Setup, dann Cleanup und dann erneut Setup aus, so als würde die Komponente gemountet, unmountet und sofort wieder gemountet.
Die doppelte Ausführung tritt ausschließlich in Development-Builds auf. In Production läuft ein Effect einmal beim Mounten der Komponente und danach nur dann erneut, wenn sich seine Dependencies ändern oder die Komponente neu gemountet wird.
StrictMode ist optional. Er gilt nur für Komponenten, die in <StrictMode> eingeschlossen sind. Viele Projekte sind jedoch standardmäßig davon umschlossen. Die main.jsx des Vite-React-Templates rendert <App /> innerhalb von <StrictMode>. Der Next.js App Router aktiviert StrictMode seit Next.js 13.5.1 standardmäßig. Bei Apps mit dem Pages Router muss StrictMode explizit über reactStrictMode: true aktiviert werden.
Der Durchlauf prüft, ob Ihr Cleanup tatsächlich funktioniert. Wenn sich ein Effect fehlerhaft verhält, sobald React Setup, Cleanup und Setup ausführt, verhält er sich auch fehlerhaft, wenn ein Nutzer die Seite verlässt und zurückkehrt. Ebenso wird er sich fehlerhaft verhalten, wenn Codeänderungen ihn im Entwicklungsmodus erneut auslösen: Die Next.js-Dokumentation zu Fast Refresh besagt, dass Effects eine gelegentliche erneute Ausführung vertragen sollten, und weist darauf hin, dass StrictMode genau das erzwingt. Wenn Sie genauer wissen möchten, wann das Cleanup im Verhältnis zum Paint ausgeführt wird, lesen Sie useEffect vs. useLayoutEffect.
Wie behebt man einen useEffect, der zweimal ausgeführt wird?
Um einen useEffect zu korrigieren, der zweimal ausgeführt wird, geben Sie ihm eine Cleanup-Funktion, die rückgängig macht, was das Setup angestoßen hat. Für jeden typischen Fehler gibt es einen konkreten Fix:
| Was Sie im Entwicklungsmodus sehen | Was tatsächlich falsch ist | Fix |
|---|---|---|
| Zwei Fetches, eventuell veraltete Daten | Nichts bricht den laufenden Request ab | AbortController oder ein ignore-Flag im Cleanup |
| Ein Listener oder Abonnement feuert zweimal | Es wird nie entfernt | Im Cleanup entfernen |
| Ein Zähler endet bei 2 | Die Logik ist gar kein Side Effect | In einen Event-Handler verschieben |
Ein Fetch, der zweimal ausgelöst wird
Für einen Fetch, der in useEffect zweimal ausgelöst wird, gibt es zwei Lösungen. Die erste besteht darin, den Request im Cleanup abzubrechen. Wird ein Fetch abgebrochen, wird das Promise mit einer AbortError-DOMException zurückgewiesen, die Sie gefahrlos ignorieren können:
useEffect(() => {
const controller = new AbortController();
fetch(`/api/products/${id}`, { signal: controller.signal })
.then((res) => res.json())
.then((data) => setProduct(data))
.catch((err) => {
if (err.name !== 'AbortError') throw err;
});
return () => controller.abort();
}, [id]);
Die andere Option ist das ignore-Flag. Dieses Muster verwendet auch Reacts Leitfaden zur Synchronisierung mit Effects für den Datenabruf:
useEffect(() => {
let ignore = false;
fetch(`/api/products/${id}`)
.then((res) => res.json())
.then((data) => {
if (!ignore) setProduct(data);
});
return () => {
ignore = true;
};
}, [id]);
Auch nach beiden Fixes sehen Sie im Entwicklungsmodus weiterhin zwei Requests. Mit AbortController wird der erste Request abgebrochen. Mit ignore werden beide Requests abgeschlossen, aber nur der zweite darf den State setzen. In Production wird nur ein Request gesendet. Dasselbe Cleanup verhindert außerdem, dass eine langsame Response eine neuere überschreibt, wenn sich id ändert.
Ein Listener, der ein Leak verursacht
Ein Event-Listener, der in useEffect hinzugefügt wird, verursacht ein Leak, sofern das Cleanup ihn nicht wieder entfernt. Deklarieren Sie den Handler innerhalb des Effects, damit das Cleanup genau dieselbe Funktionsreferenz entfernt, die das Setup hinzugefügt hat:
useEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
Ein Zähler, der doppelt hochzählt
// Before: ends at 2 in development
useEffect(() => {
setCount((c) => c + 1);
}, []);
Das Setup läuft im Entwicklungsmodus zweimal, also wird auch das Inkrement zweimal ausgeführt. Kein Cleanup kann den Zähler wieder „zurückzählen“, und genau das zeigt Ihnen, dass dieser Effect gar nicht existieren sollte. Platzieren Sie das Inkrement dort, wo das auslösende Ereignis stattfindet:
<button onClick={() => setCount((c) => c + 1)}>Add</button>
Sollten Sie StrictMode entfernen oder einen useRef-Guard einbauen?
Nein. Beide Ansätze lassen die doppelte Ausführung verschwinden, der eigentliche Bug bleibt jedoch bestehen.
Wenn Sie StrictMode deaktivieren, sei es durch Entfernen des Wrappers oder durch reactStrictMode: false in Ihrer Next.js-Konfiguration, entfernen Sie lediglich den Test. Den Effects fehlt weiterhin das Cleanup.
Der useRef-Guard ist die verlockendere Option:
// Avoid: hides missing cleanup
const hasRun = useRef(false);
useEffect(() => {
if (hasRun.current) return;
hasRun.current = true;
subscribe();
}, []);
Der Guard überspringt im Entwicklungsmodus das zweite Setup, sodass die Konsole sauber aussieht. Ein useRef-Flag, das die zweite Ausführung überspringt, behebt jedoch kein fehlendes Cleanup. Es verschleiert das Problem im Entwicklungsmodus und lässt das Leak bei jedem echten Remount bestehen. Eine Ref gehört zu genau einer Komponenteninstanz. Wenn eine Route in Production unmountet und erneut gemountet wird, erhält die neue Instanz eine neue Ref, und der Effect wird erneut ausgeführt. Da weiterhin kein Cleanup existiert, wird das alte Abonnement nie entfernt.
In einem Session Replay einer App mit fehlendem Effect-Cleanup kann sich dieser Bug als doppelte Requests oder veraltete Daten nach Vor- und Zurück-Navigation zeigen. Das ist genau der Bug, den StrictMode im Entwicklungsmodus sichtbar macht.
Wann Sie keinen Effect brauchen
Viele Effects, die zweimal ausgelöst werden, hätten nie Effects sein sollen. Effects dienen dazu, sich mit etwas außerhalb von React zu synchronisieren, weil die Komponente auf dem Bildschirm angezeigt wird. You Might Not Need an Effect behandelt die beiden größten Gruppen unnötiger Effects.
Ereignisgesteuerte Logik gehört in Event-Handler. Ein Formular absenden, nach „In den Warenkorb“ eine Toast-Benachrichtigung anzeigen oder einen Zähler erhöhen geschieht, weil der Nutzer etwas getan hat, und nicht, weil eine Komponente gerendert wurde.
Werte, die sich aus Props oder State berechnen lassen, gehören ins Rendering:
// Before: extra state, extra render, effect runs twice
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(first + ' ' + last);
}, [first, last]);
// After: computed during render
const fullName = first + ' ' + last;
Ein schneller Check: Ist Ihr Effect korrekt?
Um zu prüfen, ob ein useEffect korrekt ist, fügen Sie sowohl im Setup als auch im Cleanup ein Log ein:
useEffect(() => {
console.log('setup');
return () => console.log('cleanup');
}, []);
Im Entwicklungsmodus sollte die Konsole Folgendes anzeigen:
setup
cleanup
setup
Ein Production-Build protokolliert setup nur einmal. Wenn Ihre Logs Setup, Cleanup, Setup zeigen und die UI am Ende korrekt ist, funktioniert der Effect wie vorgesehen, und es gibt nichts zu beheben.
Fazit
Ein useEffect, der im Entwicklungsmodus zweimal ausgeführt wird, ist StrictMode, der prüft, ob Ihr Cleanup funktioniert. Unterdrücken Sie diese Prüfung nicht. Gehen Sie jeden Effect durch, der zweimal feuert, und sorgen Sie dafür, dass er den Check besteht: Brechen Sie veraltete Requests ab oder ignorieren Sie sie, entfernen Sie, was Sie hinzufügen, und verlagern Sie ereignisgesteuerte Logik und abgeleitete Werte vollständig aus Effects heraus. Sobald Ihre Effects korrekt aufräumen, sollten Sie React Error Boundaries ergänzen, damit eine Komponente, die beim Rendern einen Fehler wirft, nicht die gesamte Seite lahmlegt.
FAQs
Warum wird mein useEffect in Production zweimal ausgeführt, obwohl StrictMode dort nicht aktiv ist?
In Production wird ein Effect erneut ausgeführt, wenn sich der Wert einer seiner Dependencies ändert oder die Komponente neu gemountet wird. Weicht eine Dependency vom letzten Render ab, läuft der Effect erneut. Objekte und Funktionen, die während des Renderns erzeugt werden, gelten daher jedes Mal als neue Werte. Auch eine Änderung der key-Prop oder bedingtes Rendering, das die Komponente entfernt und wieder hinzufügt, führt zu einem Remount und damit zu einem erneuten Setup.
Sollte ich verhindern, dass ein Analytics-Event im Entwicklungsmodus zweimal ausgelöst wird?
Nein. Die React-Dokumentation empfiehlt, einen Analytics-Aufruf für Seitenbesuche in seinem Effect zu belassen. Nutzer können nicht erkennen, ob er einmal oder zweimal ausgeführt wurde, und ein Production-Build sendet jeden Besuch nur einmal. Ihr Entwicklungsrechner sollte ohnehin keine Events in Ihre Production-Metriken senden. Wenn Sie die Events debuggen müssen, testen Sie auf einem Staging-Build im Production-Modus oder deaktivieren Sie StrictMode vorübergehend.
Wird useLayoutEffect unter StrictMode ebenfalls zweimal ausgeführt?
Ja. Die React-Referenz zu useLayoutEffect beschreibt dasselbe Verhalten im Entwicklungsmodus wie bei useEffect: Bei aktiviertem StrictMode führt React zunächst einen Setup- und Cleanup-Durchlauf aus und anschließend das eigentliche Setup. Layout-Effects benötigen dasselbe passende Cleanup, etwa das Trennen eines Observers oder das Zerstören einer Widget-Instanz eines Drittanbieters. Ein Wechsel zu useLayoutEffect ändert nur, wann der Effect im Verhältnis zum Paint ausgeführt wird, nicht aber, wie oft er im Entwicklungsmodus läuft.
Kann ich StrictMode für eine einzelne Komponente deaktivieren und für den Rest der App beibehalten?
Nein. Sobald ein Baum in StrictMode eingeschlossen ist, durchläuft jede darin enthaltene Komponente die Prüfungen, und keine einzelne Komponente kann sich davon ausnehmen. Sie können jedoch den StrictMode-Wrapper weiter nach unten im Baum verschieben, sodass er nur bestimmte Teile der App abdeckt. Wenn StrictMode nicht die Root umschließt, lässt React beim ersten Mount die zusätzliche Effect-Ausführung weg. Diese würde sonst dazu führen, dass die Effects von Kindkomponenten zweimal feuern, während die Effects ihrer Elternkomponenten nur einmal feuern, was in Production nicht vorkommen kann.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k