Double Invocation: warum Bugs dadurch sichtbar werden
„Meine App verhält sich im Development-Modus seltsam, im Production läuft sie einwandfrei" – dieser Satz ist ein Warnsignal, kein Entwarnung. React Strict Mode macht Bugs sichtbar, die in Production irgendwann auftreten werden. Das Doppelt-Ausführen ist kein Bug, sondern eine präzise Diagnosestrategie.
Inhaltsverzeichnis
- 1. Was React Strict Mode wirklich macht
- 2. Warum Double Invocation kein Bug ist
- 3. Der useEffect-Ablauf im Strict Mode
- 4. Welche Bugs-Kategorien sichtbar werden
- 5. Korrekte Cleanup-Funktionen schreiben
- 6. Idempotenz: Effects, die zweimal laufen können
- 7. Drittbibliotheken und Strict Mode
- 8. Strict Mode und Concurrent Features in React 18
- 9. Typische Bugs im Vergleich: falsch vs. korrekt
- 10. Zusammenfassung
- 11. FAQ
1. Was React Strict Mode wirklich macht
<React.StrictMode> ist ein Development-only-Werkzeug, das drei Dinge tut: Es führt Render-Funktionen und den Body von Komponenten zweimal aus (Double Invocation), es mountet und unmountet Effects direkt nach dem ersten Mount noch einmal, und es warnt bei der Verwendung veralteter APIs. Keines dieser Verhalten tritt im Production-Build auf. Das ist beabsichtigt: Strict Mode soll Fehler in der Entwicklungsphase aufdecken, die im Production-Modus erst unter bestimmten Bedingungen sichtbar werden – oder gar nicht, bis sie zu Datenverlust oder Inkonsistenz führen.
Ein weit verbreitetes Missverständnis: „Strict Mode ist nervig, ich deaktiviere ihn." Wer Strict Mode deaktiviert, deaktiviert einen Fehler-Detektor. Die Fehler, die Strict Mode sichtbar macht, existieren weiterhin im Code. In Production tauchen sie auf, wenn React intern Concurrent Features nutzt – Komponenten können mehrfach gerendert, unterbrochen und fortgesetzt werden. Strict Mode simuliert dieses Verhalten gezielt im Development-Modus, damit Probleme früh gefunden und behoben werden.
2. Warum Double Invocation kein Bug ist
Double Invocation – das doppelte Ausführen von Render-Funktionen – macht einen fundamentalen Anspruch sichtbar: Render-Funktionen müssen pure sein. Eine pure Funktion gibt bei denselben Inputs immer denselben Output zurück und hat keine Seiteneffekte. Wenn eine Render-Funktion einen Zähler inkrementiert, eine globale Variable schreibt oder einen API-Request abschickt, ist sie nicht pure. Dieses Problem existiert unabhängig vom Strict Mode, wird aber erst durch die doppelte Ausführung offensichtlich.
Die Idee hinter Double Invocation: Im Concurrent Mode von React 18 kann React die Ausführung einer Render-Funktion unterbrechen, verwerfen und neu starten. Das passiert zum Beispiel, wenn eine höher priorisierte Aktualisierung eingeht. Eine nicht-pure Render-Funktion – die Seiteneffekte produziert – verhält sich unter diesen Bedingungen falsch: Seiteneffekte werden doppelt ausgeführt, Zähler falsch inkrementiert, globale State korrumpiert. Strict Mode erzwingt das doppelte Ausführen im Development-Modus, damit solche Probleme früh erkannt werden, statt erst dann, wenn Concurrent Mode intern aktiv wird.
// Demonstration: impure render function exposed by Strict Mode
let renderCount = 0; // Global mutable state — anti-pattern
// WRONG: Side effect in render body
function BadCounter() {
renderCount++; // Mutated in render — runs twice in Strict Mode
console.log('Render count:', renderCount); // Logs 1, then 2, in one mount
return <div>Count: {renderCount}</div>;
// Shows 2 on first render — double invocation exposes the bug
}
// RIGHT: Pure render, effects in useEffect
function GoodCounter() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
// Side effects belong here, not in render
document.title = `Count: ${count}`;
}, [count]);
// Render is pure: same state → same output, no side effects
return (
<div>
<span>{count}</span>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
</div>
);
}
3. Der useEffect-Ablauf im Strict Mode
Der useEffect-Ablauf im Strict Mode ist das, was die meisten Entwickler am meisten verwirrt und überrascht. In der Produktion läuft ein Effect nach dem Mount einmal: Mount → Effect. Im Strict Mode im Development-Modus läuft es so: Mount → Effect → Cleanup → Effect. Das Muster ist also: Mount, Effect ausführen, Cleanup ausführen (als ob die Komponente unmountet), dann den Effect erneut ausführen. Das simuliert ein Unmount und Re-Mount der Komponente.
Warum dieses Muster? React 18 plant Features, bei denen Komponenten off-screen verschoben und wieder aktiviert werden können – ähnlich wie ein Tab, der nicht sichtbar ist, deaktiviert werden kann und beim Wiedersichtbarwerden seinen State wiederherstellt. Das bedeutet, Effects müssen ihren Zustand über Cleanup vollständig aufräumen können. Wenn der Effect nach einem Cleanup nicht denselben Zustand wiederherstellt wie nach dem ersten Mount, liegt ein Bug vor, der durch das Strict-Mode-Muster sofort sichtbar wird.
4. Welche Bugs-Kategorien sichtbar werden
Strict Mode macht vier Hauptkategorien von Bugs sichtbar. Erstens: Fehlende Cleanup-Logik in Effects. Ein Effect, der ein Intervall startet, ein Abonnement registriert oder eine Verbindung öffnet, ohne im Cleanup aufzuräumen, akkumuliert Ressourcen bei jedem Re-Mount. Im Strict Mode passiert das sofort: Zwei Intervalle laufen parallel, zwei WebSocket-Verbindungen sind offen, zwei Event-Listener registriert. In Production tritt das auf, wenn eine Komponente unmountet und wieder mountet – zum Beispiel beim Navigation in Single-Page-Apps.
Zweitens: Seiteneffekte im Render-Body. Alles, was in der Render-Funktion ausgeführt wird, muss frei von Seiteneffekten sein. Drittens: State-Inkonsistenz durch nicht-idempotente Effects. Wenn ein Effect State setzt, der nicht dieselbe Startbedingung wiederherstellt wie der Initialzustand, zeigt sich das im doppelten Effect-Lauf als falscher Ausgangszustand. Viertens: Race Conditions bei async Effects. Wenn ein Effect eine async Operation startet und die Komponente unmountet bevor die Operation fertig ist, kann der Callback State auf einer unmounteten Komponente setzen – der Cleanup muss solche laufenden Operationen abbrechen.
// Cleanup patterns for common Strict Mode bug categories
// 1. Timer: must clear on cleanup
function TimerComponent() {
const [seconds, setSeconds] = React.useState(0);
React.useEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1); // Functional update — safe for double invocation
}, 1000);
// Cleanup: Strict Mode runs this between first and second Effect mount
return () => clearInterval(interval);
// Without cleanup: two intervals run after Strict Mode's double mount
}, []);
return <div>Seconds: {seconds}</div>;
}
// 2. Async: abort controller prevents state-on-unmounted
function DataFetcher({ userId }: { userId: string }) {
const [data, setData] = React.useState<User | null>(null);
React.useEffect(() => {
const controller = new AbortController();
fetch(`/api/users/${userId}`, { signal: controller.signal })
.then(res => res.json())
.then(setData)
.catch(err => {
if (err.name !== 'AbortError') console.error(err);
});
// Cleanup: abort in-flight request on unmount or re-run
return () => controller.abort();
// Without cleanup: race condition on fast userId changes
}, [userId]);
return <div>{data?.name ?? 'Loading...'}</div>;
}
5. Korrekte Cleanup-Funktionen schreiben
Die Cleanup-Funktion eines useEffect ist das Gegenstück zum Setup: Sie soll den Zustand vollständig wiederherstellen, als wäre der Effect nie gelaufen. Dieser Symmetrie-Anspruch ist der Schlüssel zu korrekten Effects. Für jeden Ressourcentyp gibt es ein entsprechendes Cleanup-Pattern: Intervalle mit clearInterval, Timeouts mit clearTimeout, Event-Listener mit removeEventListener, Abonnements mit der vom Observable bereitgestellten Unsubscribe-Funktion, WebSocket-Verbindungen mit ws.close().
Ein häufiger Fehler: Die Cleanup-Funktion räumt zwar auf, aber mit der falschen Instanz. Das passiert, wenn die Ressource außerhalb des Effects in einem ref gespeichert wird und der Effect die Referenz überschreibt. Die korrekte Lösung: jede Ressource als lokale Variable im Effect deklarieren und genau diese Variable in der Cleanup-Funktion verwenden. Die Closure garantiert, dass die Cleanup-Funktion dieselbe Instanz kennt, die der Effect geöffnet hat. Diese Symmetrie macht Effects robust unter Concurrent Mode und nachvollziehbar für das Strict-Mode-Doppel-Muster.
6. Idempotenz: Effects, die zweimal laufen können
Idempotenz bedeutet, dass eine Operation mehrfach ausgeführt werden kann, ohne den Gesamtzustand zu verändern. Für React Effects: Ein Effect, der zweimal läuft, soll denselben Endzustand erzeugen wie ein Effect, der einmal läuft – vorausgesetzt, der Cleanup läuft dazwischen. Das ist die formale Anforderung, die Strict Mode überprüft. Wenn nach dem Zyklus „Effect → Cleanup → Effect" der Zustand sich von „einmaliger Effect" unterscheidet, liegt eine Idempotenz-Verletzung vor.
Ein klassisches nicht-idempotentes Muster: Ein Effect fügt ein DOM-Element ein. Beim ersten Lauf wird es eingefügt. Cleanup entfernt es. Beim zweiten Lauf wird es erneut eingefügt. Das ist korrekt. Ein nicht-korrektes Beispiel: Ein Effect fügt eine CSS-Klasse hinzu. Cleanup entfernt sie nicht. Beim zweiten Lauf ist die Klasse bereits vorhanden und wird nicht erneut hinzugefügt – aber der Zustand ist korrekt. Gefährlicher ist: Ein Effect registriert einen globalen Event-Listener. Cleanup entfernt ihn. Zweiter Lauf registriert ihn erneut. Korrekt. Aber ohne Cleanup: Zwei Listener, doppelte Events.
7. Drittbibliotheken und Strict Mode
Eine häufige Quelle von Strict-Mode-Problemen sind Drittbibliotheken, die nicht für doppelte Effect-Ausführung ausgelegt sind. Ältere Bibliotheken, die DOM-Elemente direkt manipulieren (Chart-Bibliotheken, Map-SDKs, Rich-Text-Editoren), initialisieren ihre Instanz typischerweise im componentDidMount oder einem useEffect und erwarten, dass diese Initialisierung genau einmal erfolgt. Im Strict Mode erfolgt sie zweimal – was zu Fehlern, doppelten Initialisierungen oder Abstürzen führt.
Die korrekte Lösung ist nicht, Strict Mode zu deaktivieren, sondern den Effect korrekt zu schreiben. Das Muster: Bibliothek in einem ref speichern, in der Cleanup-Funktion die Bibliotheks-Destroy-Methode aufrufen und das Ref auf null setzen. Wenn die Bibliothek keine Destroy-Methode hat, ist das selbst ein Bug der Bibliothek. In solchen Fällen hilft das Pattern mit einer boolean-Flag im Effect-Closure: let initialized = false; if (!initialized) { /* init */ initialized = true; } – das verhindert doppelte Initialisierung auch ohne Cleanup-Methode, ist aber ein Workaround, nicht die saubere Lösung.
8. Strict Mode und Concurrent Features in React 18
React 18 hat die Bedeutung von Strict Mode wesentlich erhöht. Mit Concurrent Features wie startTransition, useDeferredValue und dem Suspense-basierten Streaming kann React Render-Zyklen unterbrechen, neu priorisieren und wiederholen. Ein State-Update mit niedrigerer Priorität kann verworfen werden, wenn ein Update mit höherer Priorität eingeht – das bedeutet, die Render-Funktion wird mehrfach ausgeführt, und nicht alle davon führen zu einem Commit. Render-Funktionen müssen unter diesen Bedingungen pure sein, sonst entstehen nicht reproduzierbare Bugs.
Strict Mode in React 18 simuliert zusätzlich das Off-Screen-Verhalten für zukünftige React-Features: Komponenten werden deaktiviert und reaktiviert, ihre Effects werden dabei cleanup-ed und neu gemounted. Wer seinen Code Strict-Mode-kompatibel hält, bereitet sich auf zukünftige React-Features vor, ohne späteren Refactoring-Aufwand. Das ist der strategische Grund, warum React StrictMode auch nach Jahren noch alle neuen Checks bekommt: Es ist ein Kompatibilitäts-Prüfer für die Zukunft der React-Architektur.
9. Typische Bugs im Vergleich: falsch vs. korrekt
Die folgende Tabelle zeigt häufige Strict-Mode-Probleme und ihre korrekte Lösung. Jedes dieser Muster funktioniert im Production-Build zufällig oder zeitweise korrekt – und schlägt unter Concurrent Features oder beim Re-Mount fehl.
| Kategorie | Falsches Muster | Korrektes Muster | Symptom in Strict Mode |
|---|---|---|---|
| Intervall | setInterval ohne clearInterval | return () => clearInterval(id) | Zwei Intervalle, doppelt schnelle Updates |
| Async Fetch | Kein AbortController | AbortController + return () => abort() | Race Condition, State auf unmounteten Komponenten |
| Event Listener | addEventListener ohne removeEventListener | return () => removeEventListener() | Doppelte Event-Ausführung |
| Render-Seiteneffekt | API-Call im Render-Body | API-Call nur in useEffect | Doppelter API-Call beim Mount |
| Drittbibliothek | Init ohne Destroy-Cleanup | return () => instance.destroy() | Doppelte Initialisierung, Fehler in Bibliothek |
Das entscheidende Muster hinter allen korrekten Lösungen ist identisch: Symmetrie zwischen Setup und Cleanup. Was der Effect öffnet, muss der Cleanup schließen. Was der Effect registriert, muss der Cleanup deregistrieren. Was der Effect startet, muss der Cleanup stoppen. Diese Symmetrie macht Effects robust für alle Szenarien, die React intern durchläuft – Strict Mode, Concurrent Mode und zukünftige Off-Screen-Features.
Mironsoft
React Code Review · useEffect-Audit · Strict-Mode-Kompatibilität
React-App Strict-Mode-kompatibel machen?
Wir auditieren eure React-Codebase auf Strict-Mode-Inkompatibilitäten, fehlende Cleanup-Logik und nicht-pure Render-Funktionen – und beheben die Probleme bevor sie in Production auftreten.
Effect-Audit
Alle useEffect-Hooks auf fehlende Cleanup-Logik und Idempotenz prüfen
Render-Review
Seiteneffekte im Render-Body identifizieren und in korrekten Hook-Kontext verschieben
Bibliotheks-Integration
Drittbibliotheken Strict-Mode-kompatibel integrieren mit korrekten Destroy-Patterns
10. Zusammenfassung
React Strict Mode ist kein optionaler Komfort, sondern ein Diagnosewerkzeug, das Fehler aufdeckt, die in Production unter Concurrent Mode auftreten werden. Double Invocation erzwingt pure Render-Funktionen – Seiteneffekte im Render-Body werden durch doppelte Ausführung sofort sichtbar. Die doppelte Effect-Ausführung (Mount → Effect → Cleanup → Effect) prüft Cleanup-Korrektheit und Idempotenz: jeder Effect muss seinen Zustand vollständig über seine Cleanup-Funktion aufräumen können.
Die vier Hauptkategorien von Bugs, die Strict Mode aufdeckt: fehlende Cleanup-Logik, Seiteneffekte im Render, nicht-idempotente Effects und Race Conditions bei async Operations. Das korrekte Muster für alle Effects folgt demselben Prinzip: Symmetrie zwischen Setup und Cleanup. Was geöffnet wird, wird geschlossen. Was registriert wird, wird deregistriert. Was gestartet wird, wird gestoppt. Wer Effects so schreibt, schreibt Code, der auch in zukünftigen React-Versionen mit Off-Screen-Rendering und erweitertem Concurrent Mode korrekt funktioniert.
React Strict Mode — Das Wichtigste auf einen Blick
Double Invocation
Render-Funktionen zweimal ausführen – macht Seiteneffekte im Render sichtbar. Render muss pure sein: gleicher Input → gleicher Output, keine Nebeneffekte.
Effect-Zyklus
Mount → Effect → Cleanup → Effect. Cleanup muss Zustand vollständig aufräumen. Symmetrie: was Setup öffnet, schließt Cleanup.
Idempotenz
Effect zweimal mit Cleanup dazwischen = gleicher Endzustand wie einmaliger Effect. Intervalle, Listener, Verbindungen: immer Cleanup-Gegenstück.
Concurrent Mode
StrictMode simuliert React-18-Internals: unterbrechbares Rendering, Off-Screen-Features. Strict-Mode-kompatibel = zukunftssicher für künftige React-Features.