Double Invocation: Why Bugs Become Visible
"My app behaves strangely in development mode, but runs flawlessly in production" is a warning sign, not a reason to relax. React Strict Mode makes visible bugs that will eventually surface in production. Running things twice is not a bug, it is a precise diagnostic strategy.
Table of Contents
- 1. What React Strict Mode Really Does
- 2. Why Double Invocation Is Not a Bug
- 3. The useEffect Sequence in Strict Mode
- 4. Which Categories of Bugs Become Visible
- 5. Writing Correct Cleanup Functions
- 6. Idempotence: Effects That Can Run Twice
- 7. Third-Party Libraries and Strict Mode
- 8. Strict Mode and Concurrent Features in React 18
- 9. Typical Bugs Compared: Wrong vs. Correct
- 10. Summary
- 11. FAQ
1. What React Strict Mode Really Does
<React.StrictMode> is a development-only tool that does three things: it runs render functions and the body of components twice (double invocation), it mounts and unmounts effects once more right after the first mount, and it warns about the use of deprecated APIs. None of this behavior occurs in the production build. That is intentional: Strict Mode is meant to surface errors during development that would otherwise only become visible in production under certain conditions, or not at all, until they lead to data loss or inconsistency.
A widespread misconception is: "Strict Mode is annoying, I will disable it." Whoever disables Strict Mode disables an error detector. The errors that Strict Mode makes visible continue to exist in the code. In production they surface once React uses concurrent features internally, since components can be rendered, interrupted, and resumed multiple times. Strict Mode deliberately simulates this behavior in development mode so that problems are found and fixed early.
2. Why Double Invocation Is Not a Bug
Double invocation, the doubled execution of render functions, makes a fundamental requirement visible: render functions must be pure. A pure function always returns the same output for the same inputs and has no side effects. If a render function increments a counter, writes to a global variable, or fires off an API request, it is not pure. This problem exists independently of Strict Mode, but only becomes obvious through the doubled execution.
The idea behind double invocation: in React 18's concurrent mode, React can interrupt, discard, and restart the execution of a render function. This happens, for example, when a higher-priority update comes in. A non-pure render function, one that produces side effects, behaves incorrectly under these conditions: side effects run twice, counters are incremented incorrectly, global state gets corrupted. Strict Mode enforces the doubled execution in development mode so that such problems are caught early, rather than only once concurrent mode becomes active internally.
// Demonstration: impure render function exposed by Strict Mode
let renderCount = 0; // Global mutable state, an 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. The useEffect Sequence in Strict Mode
The useEffect sequence in Strict Mode is what confuses and surprises most developers the most. In production, an effect runs once after mount: Mount → Effect. In Strict Mode in development, it runs as: Mount → Effect → Cleanup → Effect. The pattern is therefore: mount, run the effect, run the cleanup (as if the component were unmounting), then run the effect again. This simulates an unmount and re-mount of the component.
Why this pattern? React 18 plans features where components can be moved off-screen and reactivated again, similar to a tab that becomes inactive when not visible and restores its state when it becomes visible again. That means effects must be able to fully clean up their state via cleanup. If the effect does not restore the same state after a cleanup as it did after the first mount, there is a bug that becomes immediately visible through the Strict Mode pattern.
4. Which Categories of Bugs Become Visible
Strict Mode makes four main categories of bugs visible. First: missing cleanup logic in effects. An effect that starts an interval, registers a subscription, or opens a connection without cleaning up in the cleanup function accumulates resources on every re-mount. In Strict Mode this happens immediately: two intervals run in parallel, two WebSocket connections stay open, two event listeners are registered. In production this happens when a component unmounts and mounts again, for example during navigation in single-page apps.
Second: side effects in the render body. Everything that runs inside the render function must be free of side effects. Third: state inconsistency due to non-idempotent effects. If an effect sets state that does not restore the same starting condition as the initial state, this shows up in the doubled effect run as an incorrect starting state. Fourth: race conditions in async effects. If an effect starts an async operation and the component unmounts before the operation finishes, the callback can set state on an unmounted component, so the cleanup must abort such in-flight operations.
// 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. Writing Correct Cleanup Functions
The cleanup function of a useEffect is the counterpart to setup: it should fully restore the state as if the effect had never run. This symmetry requirement is the key to correct effects. For each type of resource there is a matching cleanup pattern: intervals with clearInterval, timeouts with clearTimeout, event listeners with removeEventListener, subscriptions with the unsubscribe function provided by the observable, WebSocket connections with ws.close().
A common mistake: the cleanup function does clean up, but on the wrong instance. This happens when the resource is stored outside the effect in a ref and the effect overwrites the reference. The correct solution: declare every resource as a local variable inside the effect and use exactly that variable in the cleanup function. The closure guarantees that the cleanup function knows the same instance that the effect opened. This symmetry makes effects robust under concurrent mode and comprehensible for the Strict Mode double-run pattern.
6. Idempotence: Effects That Can Run Twice
Idempotence means that an operation can be executed multiple times without changing the overall state. For React effects: an effect that runs twice should produce the same end state as an effect that runs once, provided the cleanup runs in between. That is the formal requirement that Strict Mode checks. If the state after the cycle "Effect → Cleanup → Effect" differs from a "single effect run," there is an idempotence violation.
A classic non-idempotent pattern: an effect inserts a DOM element. On the first run it gets inserted. Cleanup removes it. On the second run it gets inserted again. That is correct. An incorrect example: an effect adds a CSS class. Cleanup does not remove it. On the second run the class is already present and does not get added again, but the state is still correct. More dangerous: an effect registers a global event listener. Cleanup removes it. The second run registers it again. Correct. But without cleanup: two listeners, doubled events.
7. Third-Party Libraries and Strict Mode
A common source of Strict Mode problems is third-party libraries that were not designed for doubled effect execution. Older libraries that manipulate DOM elements directly (chart libraries, map SDKs, rich-text editors) typically initialize their instance in componentDidMount or a useEffect and expect that initialization to happen exactly once. In Strict Mode it happens twice, which leads to errors, duplicate initializations, or crashes.
The correct solution is not to disable Strict Mode, but to write the effect correctly. The pattern: store the library instance in a ref, call the library's destroy method in the cleanup function, and set the ref back to null. If the library has no destroy method, that is itself a bug in the library. In such cases, a boolean flag pattern in the effect closure can help: let initialized = false; if (!initialized) { /* init */ initialized = true; }, which prevents double initialization even without a cleanup method, but it is a workaround, not the clean solution.
8. Strict Mode and Concurrent Features in React 18
React 18 significantly raised the importance of Strict Mode. With concurrent features such as startTransition, useDeferredValue, and Suspense-based streaming, React can interrupt render cycles, reprioritize them, and repeat them. A lower-priority state update can be discarded when a higher-priority update comes in, which means the render function runs multiple times, and not all of these runs lead to a commit. Render functions must be pure under these conditions, otherwise non-reproducible bugs arise.
Strict Mode in React 18 additionally simulates the off-screen behavior for future React features: components are deactivated and reactivated, and their effects get cleaned up and re-mounted in the process. Whoever keeps their code Strict Mode compatible prepares for future React features without later refactoring effort. That is the strategic reason why React StrictMode still gets all new checks after years: it is a compatibility checker for the future of React's architecture.
9. Typical Bugs Compared: Wrong vs. Correct
The following table shows common Strict Mode issues and their correct solution. Each of these patterns works correctly by chance or intermittently in the production build, and fails under concurrent features or on re-mount.
| Category | Wrong Pattern | Correct Pattern | Symptom in Strict Mode |
|---|---|---|---|
| Interval | setInterval without clearInterval | return () => clearInterval(id) | Two intervals, doubled update speed |
| Async Fetch | No AbortController | AbortController + return () => abort() | Race condition, state set on unmounted components |
| Event Listener | addEventListener without removeEventListener | return () => removeEventListener() | Doubled event firing |
| Render Side Effect | API call in render body | API call only inside useEffect | Doubled API call on mount |
| Third-Party Library | Init without destroy cleanup | return () => instance.destroy() | Duplicate initialization, error in library |
The decisive pattern behind all correct solutions is identical: symmetry between setup and cleanup. Whatever the effect opens, cleanup must close. Whatever the effect registers, cleanup must deregister. Whatever the effect starts, cleanup must stop. This symmetry makes effects robust for every scenario React runs through internally, Strict Mode, concurrent mode, and future off-screen features.
Mironsoft
React Code Review · useEffect Audit · Strict Mode Compatibility
Make Your React App Strict Mode Compatible?
We audit your React codebase for Strict Mode incompatibilities, missing cleanup logic, and non-pure render functions, and fix the problems before they surface in production.
Effect Audit
Check every useEffect hook for missing cleanup logic and idempotence
Render Review
Identify side effects in the render body and move them into the correct hook context
Library Integration
Integrate third-party libraries in a Strict Mode compatible way with correct destroy patterns
10. Summary
React Strict Mode is not an optional convenience, it is a diagnostic tool that surfaces errors that will occur in production under concurrent mode. Double invocation enforces pure render functions, side effects in the render body become immediately visible through the doubled execution. The doubled effect execution (Mount → Effect → Cleanup → Effect) checks cleanup correctness and idempotence: every effect must be able to fully clean up its state via its cleanup function.
The four main categories of bugs that Strict Mode uncovers are: missing cleanup logic, side effects in render, non-idempotent effects, and race conditions in async operations. The correct pattern for all effects follows the same principle: symmetry between setup and cleanup. Whatever is opened gets closed. Whatever is registered gets deregistered. Whatever is started gets stopped. Whoever writes effects this way writes code that will also work correctly in future React versions with off-screen rendering and extended concurrent mode.
React Strict Mode: The Key Takeaways
Double Invocation
Runs render functions twice, exposing side effects in render. Render must be pure: same input to same output, no side effects.
Effect Cycle
Mount → Effect → Cleanup → Effect. Cleanup must fully clean up the state. Symmetry: whatever setup opens, cleanup closes.
Idempotence
Effect running twice with cleanup in between equals the same end state as a single effect run. Intervals, listeners, connections: always a matching cleanup.
Concurrent Mode
StrictMode simulates React 18 internals: interruptible rendering, off-screen features. Strict Mode compatible means future-proof for upcoming React features.