StrictMode
What you'll learn
- What StrictMode is: a development-only X-ray that runs parts of your code twice on purpose
- The three double-invocations and the exact bug each one exposes
- Why double-invoking is such a clever detector of impurity
- What StrictMode does not do, and how to act on what it finds
Some React bugs are invisible. A mutated prop, a side effect in render, a missing effect cleanup, the app works, the demo passes, and the bug detonates months later under concurrent rendering or a fast user. React can't stop you from writing impure code, but it can do the next best thing: run your code twice in development and let the impurity expose itself. That's StrictMode.
What StrictMode is
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>
);
Jargon: "StrictMode". A wrapper component that activates extra development-only checks for its subtree. It renders nothing to the DOM, changes nothing in production, and exists purely to make certain bugs visible early.
You can wrap the whole app (usual) or any subtree (useful when adopting it incrementally in an old codebase). Everything below is development-only behavior. In production builds, StrictMode is a no-op with zero cost.
The X-ray: three double-invocations
1. Component bodies render twice
In development, every component function inside a StrictMode subtree is invoked twice per render. The first result is used; the second is thrown away. This catches impure renders, code that mutates things outside itself.
// A module-level array: shared, outside React's world
const auditLog = [];
function TodoItem({ todo }) {
// ❌ Side effect during render — looks harmless, "works"
auditLog.push(`rendered ${todo.id}`);
return <li>{todo.text}</li>;
}
What happens:
- Without StrictMode: each render pushes once. The log looks plausible. The bug, render touching module state, is invisible. Under concurrent rendering (Part 4), React may render this component and discard the work; now the log has phantom entries and nobody knows why.
- With StrictMode: the body runs twice per render, so every entry appears doubled. Your audit log visibly doubles on day one of development. The fingerprint of the impurity, on the screen, immediately.
The lesson generalizes: any render-time mutation, pushing to arrays, incrementing counters, writing to refs, mutating props, produces doubled evidence under StrictMode. Pure renders produce identical output both times and leave no trace.
2. State updaters, reducers, and initializers run twice
The same X-ray applies to the functional pieces of state management: useState initializers, updater functions, and reducer bodies. These must be pure, React may call them at times you don't expect.
const seen = [];
function Counter() {
const [count, setCount] = useState(0);
function increment() {
// ❌ Side effect hidden inside an updater
setCount((c) => {
seen.push(c);
return c + 1;
});
}
return <button onClick={increment}>Count: {count}</button>;
}
What happens:
- Click once. Without StrictMode:
seengets one entry. Looks correct. - With StrictMode: the updater runs twice in development, so
seengets two entries per click. The side effect you smuggled into a "pure" function is now measurable. - Notice what does not break:
countitself. The updater is pure with respect to its return value (c + 1both times), so the result is still exactlycount + 1. StrictMode corrupts only what you corrupted, it doubles the side effect, not the state. That's the precision that makes it a detector rather than a nuisance.
3. Effects run setup → cleanup → setup on mount
This is the one that floods issue trackers. On initial mount, in development, every effect in the subtree runs its setup, then immediately its cleanup, then its setup again.
import { useEffect, useState } from 'react';
function Ticker() {
const [ticks, setTicks] = useState(0);
// ❌ Missing cleanup — the interval leaks
useEffect(() => {
console.log('setup');
setInterval(() => setTicks((t) => t + 1), 1000);
}, []);
return <p>{ticks}</p>;
}
What happens, the logs tell the story:
- Mount. Setup runs:
setuplogged, interval #1 created. - StrictMode immediately runs the cleanup, but there isn't one. Interval #1 keeps ticking.
- Setup runs again:
setuplogged a second time, interval #2 created. - Now watch
ticks: it jumps by 2 every second in development. Two live intervals, one forgotten cleanup, zero ambiguity about the cause.
The fix, return a cleanup, which is what the effect contract (Part 3) always required:
useEffect(() => {
console.log('setup');
const id = setInterval(() => setTicks((t) => t + 1), 1000);
return () => {
console.log('cleanup');
clearInterval(id);
};
}, []);
Now the development mount logs setup → cleanup → setup, one interval is cleared, one remains, and ticks increments by 1, in development and production. The double-invocation didn't create a bug; it revealed that your effect was never resilient to being torn down and re-created, which React does constantly (re-mounts, navigation, Offscreen hiding, fast refresh during development).
Jargon: "effect remount simulation". StrictMode's setup→cleanup→setup cycle, which simulates what would happen if the component unmounted and remounted. Effects that can't survive it are missing cleanup, by definition.
Why double-invoking works as a detector
The design is elegant: pure code is idempotent. Run a pure function twice with the same inputs and you get the same output with zero additional evidence. Run impure code twice and the side effects double, two log entries, two interval timers, a doubled array. StrictMode doesn't analyze your code; it just gives impurity a second chance to leave a fingerprint, in an environment (development) where the fingerprint is harmless. React 19 extends the same idea a bit further: ref callbacks get the double-invoke treatment too (setup with the node, cleanup with null, setup again), catching components that stash DOM nodes and never release them, plus a few extra development checks in the same spirit.
What StrictMode does NOT do
- Nothing in production. The extra renders, double updaters, and effect remounts are stripped from production builds. Zero runtime cost, zero behavior change for users.
- It does not change the behavior of correct code. If your renders are pure and your effects have proper cleanup, StrictMode is invisible. If enabling it "breaks" your app, the app was already broken, you just couldn't see it.
- It does not double-commit. The second render pass is discarded before the DOM is touched; users never see double output. The DOM commit happens once per real update, as always.
How to act on findings
When StrictMode surfaces something, the fix is always one of two moves:
- Make the render pure. Move the side effect into an event handler or an effect; derive values instead of mutating shared state; never write to module variables or props during render.
- Add the missing cleanup. Every subscription, interval, timeout, socket, and observer created in an effect gets torn down in the returned function.
The one move that is not a fix: removing <StrictMode>. That's shooting the smoke detector. The doubled behavior in development is the price of catching, this week, the bug that would otherwise ship in six months.
Diagram
Rendered diagram (PNG hi-res):
Rendered diagram (PNG hi-res):
Common misconceptions
- Misconception: StrictMode's double render means my app has a bug. Reality: the double render itself is the detector working as designed. Only observable doubling, doubled logs, timers, mutations, indicates a bug, and it indicates one that was already there.
- Misconception: StrictMode slows down production. Reality: it's compiled away entirely in production builds. It costs nothing to users.
- Misconception: Effects running twice is React 18+ being broken. Reality: it's the intentional setup→cleanup→setup cycle proving your effects survive remounts. Two intervals after mount means you never cleaned up the first.
- Misconception: If the UI looks fine, StrictMode warnings are ignorable. Reality: impure renders and missing cleanups fail later, under concurrent rendering or real remounts, in ways that are far harder to trace than a doubled dev log.
- Misconception: StrictMode double-invokes event handlers too. Reality: no, only render bodies, state updaters/initializers/reducers, effect setup/cleanup, and (React 19) ref callbacks. Handlers already run exactly when you invoke them.
- Misconception: Wrapping a subtree in StrictMode affects its parents. Reality: checks apply only downward, to the wrapped subtree.
Why it works this way
- React can't forbid impurity, so it makes impurity loud. JavaScript will happily let a render function mutate the world. Rather than a linter you can ignore, StrictMode gives impurity visible consequences in the one environment where consequences are free.
- Double-invocation is cheap and universal. It requires no static analysis, works on any code, and exploits a mathematical fact: pure functions are idempotent, impure ones are not.
- The effect remount cycle trains the right habit. Effects must be resumable, setup, teardown, setup again, because React genuinely tears them down and re-creates them in normal operation (fast refresh, navigation, Offscreen). Development should rehearse production reality.
- Dev-only keeps the trade one-sided. All the bug-exposing value, none of the user-facing cost, which is why there's no excuse for removing it.
Try it yourself
- Add the
auditLog.pushrender to any component in a StrictMode app. Open the console, trigger one render, and count entries. Then fix it (move the logging into an effect) and confirm the doubling disappears. - Build the
Tickerwithout cleanup. Watchticksincrement by 2 per second in development. Add the cleanup; confirm it increments by 1 and the log showssetup → cleanup → setupon mount. - Write
setCount((c) => { arr.push(c); return c + 1; }), click three times, and inspectarr. Explain whycountis still correct whilearrdoubled. - Temporarily remove
<StrictMode>from the root and re-run experiments 1–3. Notice how every bug becomes invisible, then put StrictMode back.
Recap
- StrictMode is a dev-only wrapper that renders nothing and, in development, deliberately double-invokes your code to expose impurity.
- Component bodies run twice per render → impure renders (mutations, side effects) leave doubled fingerprints.
- Updaters, initializers, and reducers run twice → hidden side effects in "pure" functions surface.
- Effects run setup→cleanup→setup on mount → missing cleanups show up as doubled timers and subscriptions.
- Correct code is unaffected, and production builds strip everything. Doubling is the detector, not the disease.
- React 19 adds ref callbacks to the same treatment.
- Findings are fixed by purifying renders and adding cleanups, never by deleting StrictMode.