React Strict Mode: Why Effects Run Twice and What It Means for Cleanup
AI generated
{ }
React · Strict Mode · Effects
React Strict Mode and the Double Execution of Effects
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.

13 min read Strict Mode useEffect Cleanup functions

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 component, and it only takes effect in the development build. It does not change production behavior and produces no additional DOM, but within its subtree it activates a series of extra checks meant to surface hidden problems before they turn into real bugs in production.

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.

11. FAQ: React Strict Mode Double Effects: The Essentials at a Glance

1Is the double execution of effects under Strict Mode a bug?
No, it is a deliberate development aid built in since React 18. It simulates mount, unmount, and remount to surface effects without correct cleanup during development, instead of only in production.
2Does the double execution also happen in production?
No, Strict Mode only takes effect in the development build. In the production build, every effect runs exactly once per actual mount, the double simulation is fully absent.
3How do I recognize that an effect lacks correct cleanup?
It usually shows up as duplicate network requests in the DevTools network tab, duplicated console logs, or duplicated event listener registrations after the first mount of a component under Strict Mode.
4Should I remove Strict Mode if the duplicate output is annoying?
No, that only removes the symptom and leaves the actual missing cleanup in the code, where it can later cause unnoticed resource leaks or duplicate side effects in production.
5Do event handlers like onClick also run twice under Strict Mode?
No, Strict Mode specifically doubles render functions and the mount phase of effects, but not event handlers. A click handler still runs exactly once per actual click.
6How do I write a fetch-based effect that is Strict-Mode-safe?
Through an AbortController: the signal gets passed to fetch, and the cleanup function calls controller.abort(). That cleanly cancels the first request, triggered by the simulated unmount, before it gets processed.
7What happens if an effect ends without any return value?
Then React registers no cleanup function for that effect. If the effect creates a resource like a timer or a connection, it stays alive unused both on the simulated and the real unmount, which counts as a resource leak.
8Does the double execution also apply to useLayoutEffect?
Yes, useLayoutEffect is subject to the same mount-unmount-mount simulation under Strict Mode as useEffect, which is why every resource it creates also needs a matching cleanup function.
9Does Strict Mode noticeably slow down the development environment?
The double execution costs a bit of extra compute time in the development build, but that is barely noticeable in practice. Since Strict Mode is fully absent from the production build, it has no effect on actual application performance.
10Do I have to manually enable Strict Mode for every component?
No, it is enough to wrap the desired subtree, usually the entire application, once in . Every component rendered inside it and its effects then automatically become subject to the extra checks.