<StrictMode>

Turns on extra development-only checks for the components inside it to surface common bugs early.

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

const root = createRoot(document.getElementById('root'));
root.render(
  <StrictMode>
    <App />
  </StrictMode>
);
  • Added in React 16.3.0

Parameters

ParameterTypeDescription
$childrenReactNodeThe subtree to check. The official reference states that it accepts no props beyond the children it wraps — there is no way to configure or disable individual checks.

Return value

ReactNodeIts children, unchanged. It renders no DOM of its own and has no effect on the production build.

Changelog

  • PHP 16.3.0Introduced with deprecation warnings and double-invoked render for class lifecycle methods.
  • PHP 18.0.0Effects are mounted, cleaned up, and mounted again on the initial mount, exposing missing cleanup functions.
  • PHP 19.0.0Ref callbacks are also attached, detached, and attached again to expose missing ref cleanup.

Wrapping a tree turns on four development-only behaviours for everything inside it: components render an extra time, Effects mount then clean up then mount again, ref callbacks are re-run the same way, and deprecated APIs are reported. None of it runs in a production build.

What the checks are actually for

Each one turns a latent bug into a visible one. Double rendering breaks any component that mutates a module variable, pushes to an outer array, or otherwise depends on being called once. Re-running Effects breaks any subscription, interval or event listener without a matching cleanup — a leak that in production only shows up after the user navigates back and forth a few times.

What trips people up

The double render is not a bug and not a performance problem. The usual reaction — deleting the wrapper because a network request fires twice — hides the defect rather than fixing it. A request that must not run twice belongs in an Effect with an abort in its cleanup, or in an event handler.

There is no opt-out inside the tree, by design: a subtree cannot exclude itself. If part of your app cannot pass the checks yet, move the wrapper further down the tree rather than removing it. Note that a wrapper placed below the root is weaker: React only enables behaviour that is also possible in production, so a tree that is not wrapped at the root does not get the extra Effect mount on initial mount.

The extra render's console output is not hidden: with React DevTools installed, console.log calls from the second render call appear slightly dimmed, and DevTools offers a setting — off by default — to suppress them completely.

Example

Try it yourself
Loading editor…
Loading React…

When to use it

  • Wrap the root render so the whole app is checked for impure renders and missing cleanup.
  • Wrap one newly written feature while the rest of a legacy app is not ready for the checks.
  • Reproduce a remount to verify a subscription's cleanup actually unsubscribes.
  • Surface deprecated lifecycle and string-ref usage before upgrading React.

Frequently asked questions

Why does my component render twice in development?

Because the development-only double render is deliberately calling your component an extra time to see whether it produces the same result. It happens only in development, never in a production build, and a difference between the two calls means the render is impure.

Why does my useEffect fetch run twice on mount?

The checks mount the Effect, run its cleanup, and mount it again to prove the cleanup works. Handle it by aborting the in-flight request in the cleanup function, or move one-shot requests into event handlers — removing the wrapper only hides the missing cleanup.

Should I keep it enabled in production?

There is nothing to disable — every check is compiled out of the production build, so leaving the wrapper in place costs nothing at runtime. Remove it only if a subtree cannot yet pass the checks, and prefer moving it down the tree instead.

More examples

Wiring it at the root

The standard placement: everything rendered by App is checked. Nothing about the rendered output changes — the wrapper produces no DOM — and a production build strips the extra render, the extra Effect cycle and the deprecation checks entirely.

Example · jsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.js";

const root = createRoot(document.getElementById("root"));

root.render(
  <StrictMode>
    <App />
  </StrictMode>
);

A missing cleanup the checks expose

Leaky counts up twice as fast as Fixed, because the mount/unmount/mount cycle subscribed it twice and nothing removed the first subscription. Without the checks both components would look identical in development and the leak would only appear in production after repeated navigation.

Example · jsx
import { StrictMode, useEffect, useState } from "react";
import { createRoot } from "react-dom/client";

function createTicker() {
  const listeners = new Set();
  setInterval(() => listeners.forEach((fn) => fn()), 1000);
  return {
    subscribe(fn) {
      listeners.add(fn);
      return () => listeners.delete(fn);
    },
    count: () => listeners.size,
  };
}

const ticker = createTicker();

function Leaky() {
  const [ticks, setTicks] = useState(0);
  useEffect(() => {
    ticker.subscribe(() => setTicks((t) => t + 1));
    // No cleanup: the extra mount leaves two live subscriptions,
    // so ticks advances twice per second instead of once.
  }, []);
  return <p>Leaky: {ticks} (subscribers: {ticker.count()})</p>;
}

function Fixed() {
  const [ticks, setTicks] = useState(0);
  useEffect(() => {
    const unsubscribe = ticker.subscribe(() => setTicks((t) => t + 1));
    return unsubscribe;
  }, []);
  return <p>Fixed: {ticks}</p>;
}

function Demo() {
  return (
    <>
      <Leaky />
      <Fixed />
    </>
  );
}

// The wrapper must be at the root: React skips the extra Effect mount
// on initial mount when StrictMode is enabled only for part of the tree.
createRoot(document.getElementById("root")).render(
  <StrictMode>
    <Demo />
  </StrictMode>
);

An impure render the double call reveals

The first list shows ids that jump by more than one and change on every re-render, because the counter is incremented once per render call and the checks call each component twice. The second list is stable, since its ids come from props rather than from a side effect during render.

Example · jsx
import { StrictMode, useState } from "react";

let nextId = 0;

// Impure: reads and mutates module state during render.
function BadRow({ label }) {
  nextId += 1;
  return (
    <li>
      {label} — id {nextId}
    </li>
  );
}

// Pure: the id is derived from the props it is given.
function GoodRow({ label, id }) {
  return (
    <li>
      {label} — id {id}
    </li>
  );
}

export default function IdList() {
  const [, forceRender] = useState(0);
  const labels = ["alpha", "beta", "gamma"];

  return (
    <StrictMode>
      <button onClick={() => forceRender((n) => n + 1)}>Re-render</button>
      <ul>
        {labels.map((label) => (
          <BadRow key={label} label={label} />
        ))}
      </ul>
      <ul>
        {labels.map((label, i) => (
          <GoodRow key={label} label={label} id={i} />
        ))}
      </ul>
    </StrictMode>
  );
}

Discussion

  • Be the first to comment on this lesson.