Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild
Back to Blog Explainer

React Strict Mode: Why Your Component Renders Twice, and Why That Is the Point

Sean

Platform Writer

Aug 13, 2026
8 min read

<StrictMode> deliberately calls your components twice, runs each effect twice, and double-invokes state updater functions — in development only. It is not a bug and it does not happen in production. The double render exists to make impure rendering visible immediately, and the double effect exists to prove your cleanup function actually works. When something breaks under Strict Mode, Strict Mode found a real bug.

React Strict Mode: Why Your Component Renders Twice, and Why That Is the Point

The instinct on first encounter is to disable it. That is understandable and almost always the wrong call, because the behaviour it surfaces is exactly the behaviour that breaks under concurrent rendering later.

Table of contents

What it actually does

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>
);

In development it triggers three behaviours:

  • Double-invokes render — component function bodies, useState initialisers, useMemo and useReducer callbacks all run twice. A pure function gives the same answer twice; an impure one does not, and the discrepancy is visible.
  • Double-invokes effects — every useEffect runs, its cleanup runs, and then it runs again. An effect without correct cleanup breaks visibly.
  • Warns about deprecated APIs — legacy string refs, findDOMNode, old context patterns.

None of it happens in a production build. This is a development-time diagnostic, and the cost is a slightly noisier console in exchange for finding a class of bug at the moment you write it.

The double render finds impure components

React assumes rendering is pure: same props and state, same output, no side effects. Break that assumption and you get bugs that appear under concurrent rendering, in ways that are extremely hard to trace back.

// Impure -- mutates something outside itself during render
let renderCount = 0;

function Counter() {
  renderCount++;                    // side effect during render
  return <p>Rendered {renderCount} times</p>;
}

// Impure -- mutates a prop
function List({ items }) {
  items.push({ id: 'sentinel' });   // caller's array now has an extra item
  return <ul>{items.map(i => <li key={i.id}>{i.name}</li>)}</ul>;
}

// Pure -- derives without mutating
function List({ items }) {
  const withSentinel = [...items, { id: 'sentinel', name: 'End' }];
  return <ul>{withSentinel.map(i => <li key={i.id}>{i.name}</li>)}</ul>;
}

Under Strict Mode the first version shows 2, 4, 6 rather than 1, 2, 3, and the second duplicates the sentinel. Both are real bugs. Without the double render they lie dormant until something else changes the render timing, at which point the connection to the cause is long gone.

The double effect finds missing cleanup

The mount-unmount-remount simulation is the more practically valuable half, because effects without cleanup leak.

// Leaks -- a second subscription is created, the first is never removed
useEffect(() => {
  const socket = new WebSocket(url);
  socket.onmessage = handleMessage;
}, [url]);

// Correct
useEffect(() => {
  const socket = new WebSocket(url);
  socket.onmessage = handleMessage;
  return () => socket.close();
}, [url]);

Strict Mode makes the leak immediate and obvious rather than something that surfaces after twenty navigations. The same applies to intervals, event listeners, and observers.

Duplicated network requests are the most-reported complaint, and the fix is an abort signal — which you wanted anyway, because it also handles the race where a fast second request resolves before a slow first one and the stale response overwrites the fresh state.

useEffect(() => {
  const controller = new AbortController();

  fetch(`/api/users/${id}`, { signal: controller.signal })
    .then(r => r.json())
    .then(setUser)
    .catch(err => {
      if (err.name !== 'AbortError') setError(err);
    });

  return () => controller.abort();
}, [id]);

Things that genuinely should run once

Some operations are not idempotent and running them twice is wrong — an analytics event, a payment intent, a one-time initialisation.

The first question is whether it belongs in an effect at all. Analytics for a user action belongs in the event handler, not an effect. An effect firing on mount will also fire on every remount, which is a different thing from “when the user did the thing”.

For genuine once-per-app-lifetime initialisation, guard it outside the component:

// Module scope -- survives remounts, runs once per page load
let initialised = false;

function App() {
  useEffect(() => {
    if (initialised) return;
    initialised = true;
    analytics.init();
  }, []);
}

// Often simpler: just do it at module scope
analytics.init();

A useRef guard is the pattern people reach for first, but a ref is per-component-instance, so it does not protect against a genuine remount. Module scope is the honest place for genuinely global initialisation.

Avoid guards as a general habit. Most effects should be idempotent, and reaching for a guard is usually a sign the logic wants to live somewhere else.

Should you turn it off?

The honest answer is: not in your own code, occasionally in someone else’s.

The case for keeping it is that concurrent React can already mount, unmount, and remount components as part of normal operation. Code that only works when effects run exactly once is code that has a latent bug in production, whether or not Strict Mode is telling you about it today.

The case for turning it off is narrower: a third-party library that is not Strict Mode safe and that you cannot fix. Even then, disabling it globally to silence one component is a large hammer — Strict Mode can be applied to a subtree rather than the whole app, so you can wrap most of your tree and exclude the problematic part.

// Scope it rather than removing it
<>
  <StrictMode>
    <MostOfTheApp />
  </StrictMode>
  <LegacyWidget />
</>

In Next.js the setting is reactStrictMode in next.config.js. Turning it off there is a decision about your whole application, and it is worth being deliberate about rather than doing it to make a console warning go away.

The pattern underneath

Strict Mode is enforcing one idea: rendering should be a pure function of props and state, and every effect should be able to be undone. Every complaint it produces is a place where that is not true.

It also has a nice property as a proxy for correctness elsewhere. Code that survives Strict Mode tends to survive fast refresh, navigation, retries, and reconnects, because they all exercise the same mount-unmount-remount path.

The same discipline applies to the server side of an application. A request handler that is not safe to retry is the backend version of an effect without cleanup — and the place you find out is a deploy, a restart, or a client retry. Runtime logs per deploy and the ability to roll back to the previous version, which is how services work on RunxBuild, is what turns that discovery into a short incident rather than a long one.

How this fits the rest of the stack

Strict Mode double-renders and double-invokes effects in development only. The double render finds components that mutate things they should not; the double effect finds effects without cleanup. Both are real bugs and both are cheaper to fix now than after concurrent rendering surfaces them.

Add AbortController to fetches, return cleanup from every subscription, and move genuinely-once initialisation to module scope. Keep it on. If you are working out what running the resulting app costs, the RunxBuild hosting calculator puts the service, database, storage, and bandwidth on one page as separate numbers.

Useful related references:

FAQ

Why does my React component render twice?

Because Strict Mode intentionally double-invokes component functions in development to reveal impure rendering. A pure component produces the same result both times, so nothing visibly changes; one that mutates external state or its props behaves differently, which is the bug being surfaced. It does not happen in production builds.

Does React Strict Mode affect production?

No. All of its behaviours — double rendering, double-invoking effects, deprecation warnings — are development-only. Production builds render and run effects once. Leaving it enabled costs nothing at runtime for your users.

How do I stop useEffect running twice?

Usually you should not stop it — make the effect idempotent instead by returning a cleanup function that undoes whatever it did. For fetches, use an AbortController and abort in cleanup, which also fixes the stale-response race. For genuinely once-per-page initialisation, guard it at module scope rather than inside the component.

Should I disable React Strict Mode?

Generally no. Concurrent React can already mount and remount components, so anything that breaks under Strict Mode has a latent production bug. If a third-party library is genuinely incompatible and unfixable, wrap only the rest of your tree in <StrictMode> rather than removing it from the whole application.

Why do I see two network requests in development?

Strict Mode runs the effect, its cleanup, and then the effect again, so a fetch inside it fires twice. Add an AbortController and call abort() in the cleanup function. This is worth doing regardless, since it also cancels stale requests when a dependency changes mid-flight.

#react#strict mode#double render#useEffect#development