Mount, unmount, mount: why this is not a bug but a deliberate development aid
Anyone who first sees a console.log inside a useEffect suddenly appear twice in the development build almost always assumes it is a bug. In reality, the double execution under Strict Mode in React 18 and 19 is deliberately built in to expose exactly the effects that lack correct cleanup and would otherwise turn into hard-to-find bugs in production later.
Table of Contents
- 1. What Strict Mode actually does
- 2. Why effects specifically run twice
- 3. How missing cleanup becomes instantly visible
- 4. Writing effects with correct cleanup
- 5. Common confusion on first contact with Strict Mode
- 6. Cleanly canceling network requests with AbortController
- 7. What Strict Mode explicitly does not double
- 8. Why Strict Mode should not simply be disabled
- 9. A practical checklist for Strict-Mode-safe effects
- 10. Summary
- 11. FAQ
1. What Strict Mode actually does
Strict Mode is not an additional library, it is a development aid built directly into React, enabled through the
One of these checks touches function components and hooks directly: React deliberately calls render functions, certain state updaters, and, since React 18, effects twice within the Strict Mode subtree. That may look contradictory on first contact, but it follows a clear logic: instead of waiting for a random race condition or a rare user interaction to expose a problem in production weeks later, Strict Mode reliably provokes the same class of bugs on every single render during development.
2. Why effects specifically run twice
Since React 18, Strict Mode additionally simulates an immediate unmount and remount for every component that mounts. Concretely, the following sequence plays out: the component mounts, its effects run, React immediately unmounts it again, calling every cleanup function along the way, and then mounts it again, running the effects a second time. Nothing of this is visible to the user in the browser, the rendered DOM stays stable, only the effect logic underneath goes through this extra cycle.
The reason is React 18's concurrent rendering model: a component can genuinely be mounted more than once in a running application, for example when state is kept in memory and a component is shown again later, or in future combination with features like offscreen rendering. Effects that do not behave correctly on a remount would break silently in production under such scenarios. The double execution in the development build forces developers to account for that case from the start, rather than discovering it only through a production incident.
3. How missing cleanup becomes instantly visible
An effect without a cleanup function, for example one that opens a WebSocket, starts an interval timer, or registers a global event listener, leaves a corpse behind at the simulated unmount: the resource from the first run stays alive while the second run creates another, identical resource. Instead of a single WebSocket connection there are suddenly two, instead of one interval there are two running in parallel, and every incoming event gets processed twice.
Without Strict Mode, this pattern would usually go unnoticed, because a component typically only mounts once in day-to-day development and the effect runs accordingly only once. Only in production, with frequent navigation between views or under certain React 18 features, would such resource leaks accumulate over time, leading to duplicate network requests, duplicated analytics events, or noticeably rising memory usage. Strict Mode makes exactly this leak visible during the very first test run, because the duplicated resource shows up immediately in the network tab or the console.
// Broken: no cleanup, the connection stays open after unmount
function ChatRoom({ roomId }) {
useEffect(() => {
const socket = createConnection(roomId);
socket.connect();
// No return -> no cleanup function!
}, [roomId]);
return <p>Connected to room {roomId}</p>;
}
// Under Strict Mode this results in two open connections
// after every mount, instead of just one.
4. Writing effects with correct cleanup
In most cases the fix is straightforward: every effect that creates a resource should return a cleanup function that tears that exact resource back down. For a WebSocket that means an explicit disconnect call, for an interval timer a clearInterval, for an event listener a removeEventListener with the exact same function reference it was registered with. React calls this function both on a real unmount and on the simulated unmount under Strict Mode.
With correct cleanup in place, the Strict Mode sequence plays out as follows: mount, the effect creates the connection, simulated unmount, cleanup closes the connection cleanly, remount, the effect creates the connection again. In the end exactly one connection exists open, even though the effect ran twice overall. That exact behavior, ending up in a clean, simple state despite double execution, is the criterion for an effect being correctly written for React 18 and 19.
// Correct: cleanup function tears the resource back down
function ChatRoom({ roomId }) {
useEffect(() => {
const socket = createConnection(roomId);
socket.connect();
return () => {
socket.disconnect();
};
}, [roomId]);
return <p>Connected to room {roomId}</p>;
}
5. Common confusion on first contact with Strict Mode
The most common reflex among developers new to this is interpreting the double execution as a bug in React itself and simply removing Strict Mode to make the console output look as expected again. That eliminates the symptom, but leaves the actual problem, the missing or broken cleanup, untouched in the code, where it eventually shows up in production anyway, just without the helpful, early warning.
A second common misunderstanding involves API calls made directly inside an effect without any cancellation logic: a fetch call that fires twice looks like a bug in the network tab, but in development mode it is expected behavior, as long as the second response gets processed correctly and does not trigger duplicate server-side side effects. For plain GET requests without side effects that is usually harmless, for POST requests with side effects like sending an email, an effect with a direct API call belongs in an explicit event handler rather than in an automatically running effect in the first place.
6. Cleanly canceling network requests with AbortController
For data-fetching effects, AbortController is the right cleanup tool: the effect creates a controller, passes its signal to fetch, and the cleanup function calls controller.abort(). Under Strict Mode this means the first request, triggered by the simulated unmount, gets cleanly canceled before its response could even be processed, while the second request runs normally and delivers the data.
This pattern solves two problems at once: it makes the effect Strict-Mode-safe, since no duplicate, unnecessary request lingers unobserved in the background, and it simultaneously prevents the race condition covered in an earlier article that occurs with rapidly changing props, for example a quickly changing ID in the URL. A canceled request throws an AbortError in modern browsers, which should be deliberately ignored in the catch block rather than treated like a genuine network error.
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch(`/api/users/${userId}`, { signal: controller.signal })
.then((res) => res.json())
.then((data) => setUser(data))
.catch((err) => {
if (err.name !== "AbortError") {
console.error("Failed to load", err);
}
});
return () => controller.abort();
}, [userId]);
return user ? <p>{user.name}</p> : <p>Loading...</p>;
}
7. What Strict Mode explicitly does not double
It matters to understand that Strict Mode does not double everything indiscriminately. Event handlers like onClick still run exactly once per click, a regular fetch call inside a handler is not doubled, and state updates coming from user interactions behave unchanged. What is affected specifically is the render function itself and the mount phase of effects, exactly the spots where React assumes deterministic, repeatable behavior.
This narrow scope is deliberate: if React doubled arbitrary imperative code inside event handlers, it would flood code that is only ever meant to run once per user action with false alarms. By focusing on render and effect mount, Strict Mode keeps its signal clearly aimed at exactly the spots where side effects without correct cleanup can actually turn into real bugs in production.
8. Why Strict Mode should not simply be disabled
The urge to remove StrictMode from an application as soon as the duplicate console output becomes annoying is understandable but counterproductive: Strict Mode surfaces problems at exactly the moment they are cheapest to fix, namely during active development of the affected component. Once removed, Strict Mode rarely comes back in practice, and the cleanup gaps that were actually present keep quietly lurking in the code.
The more economical path is to treat Strict Mode as a fixed part of the development environment and read every duplicate log line or duplicate network request as a concrete signal of missing cleanup, rather than ignoring it or configuring it away. Teams encountering Strict Mode for the first time benefit from deliberately walking through its behavior across the whole project once, systematically checking every effect for a matching cleanup function instead of fixing individual cases only when complaints come in.
9. A practical checklist for Strict-Mode-safe effects
In practice, a short, repeatable check has proven useful: does the effect create a resource, be it a connection, a timer, a listener, or a request? If so, is there a cleanup function that exactly undoes that resource? And does the effect behave identically on its second run compared to the first, without relying on a specific starting state that can only occur once?
These three questions can be asked for every effect during code review and cover the vast majority of Strict Mode problems. The table below summarizes typical effect categories and their matching cleanup pattern once more, as a quick reference for everyday use.
| Effect type | Resource created | Matching cleanup | Symptom without cleanup |
|---|---|---|---|
| WebSocket connection | socket.connect() | socket.disconnect() in the return | Duplicate open connections, duplicate events |
| Interval or timeout | setInterval / setTimeout | clearInterval / clearTimeout in the return | Two parallel timers, double execution |
| Global event listener | addEventListener | removeEventListener with the same reference | Handler gets called multiple times per event |
| Data fetching with fetch | fetch(url, { signal }) | controller.abort() in the return | A duplicate, unobserved request in the background |
Mironsoft
React architecture, performance, and Magento frontend integration
React frontends that stay fast instead of slowing down with every feature?
We review existing React applications for unnecessary re-renders, bloated bundles, and fragile state management, then build a frontend that stays performant and connects cleanly to Magento or other backends.
Performance Audit
Systematically measuring and fixing re-renders, bundle size, and load times.
State Architecture
Cleanly separating context, client state, and server state instead of mixing everything.
Magento Integration
Building robust, type-safe GraphQL or REST integration with Magento.
10. Summary
React Strict Mode Double Effects: The Essentials at a Glance
What happens
React 18 and 19 simulate, per component under Strict Mode: mount, unmount, remount, including all effects.
Why
So effects without correct cleanup reliably show up on every test run, instead of only causing problems in production.
The fix
Every effect that creates a resource needs a cleanup function that tears that exact resource back down.
What is affected
Render functions and the mount phase of effects, but not event handlers or regular user interactions.