Topic 7.4
Why StrictMode Runs Effects Twice
In one line
In development only, <StrictMode> renders every component twice and runs every effect setup → cleanup → setup when it first mounts, so missing cleanups and impure renders show up immediately; the fix is always to make the effect symmetric, never to stop it running twice.
Think of it like this
A fire drill. Nothing is burning, but the school rings the alarm on purpose to check that everyone knows the way out. If the drill reveals a locked door, you fix the door; you don't cancel the drill. StrictMode is a drill for your components: it pretends the component left and came back, to check that your effects clean up properly.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- StrictMode
- A wrapper component that turns on extra development-only checks for the components inside it.
- Development build
- The version of React used while you code: slower, with warnings and StrictMode checks.
- Production build
- The optimised version users get. StrictMode does nothing there.
- Double rendering
- StrictMode calling each component function twice to expose impure code.
- Mount → unmount → mount
- StrictMode running an effect's setup, cleanup and setup again when a component first appears.
- Symmetric effect
- An effect whose cleanup exactly undoes its setup, so setup → cleanup → setup acts like one setup.
- Silencing
- Hiding a symptom (with a ref flag or by removing StrictMode) instead of fixing the cause.
- Ignore flag
- A variable an effect's cleanup sets to
trueso a late response from an old run is thrown away.
Step by step
01StrictMode wraps the app
You'll find this in every new project's entry file. Everything inside <StrictMode> gets the checks.
createRoot(document.getElementById("root")).render(
<StrictMode>
<App />
</StrictMode>,
);02On first mount, effects run three steps
React runs the setup, then the cleanup, then the setup again. This happens once per component, on its first mount, and only in development.
03Correct effects end in the same state
With a proper cleanup, the result after three steps is one open connection, exactly like production. Without cleanup, it's two: the drill found the locked door.
04Fix the effect, don't silence it
Ask what the effect started and add the matching undo. For data fetching, the undo is to ignore (or abort) the first request's response.
Try it yourself
- 1
Remove StrictMode
In the first example, render
<ChatRoom roomId="general" />without<StrictMode>. Predict the console. (One render line and one connect line: this is what production does.) - 2
Fix the leaking room
In the second example, add
return () => connection.disconnect();. Predict the final count before you run. (1.) - 3
Break the purity check
In the first example, add
const seen = [];at the top of the module andseen.push(roomId);insideChatRoom's body, then logseen.lengthfrom asetTimeout. Predict the number. (2: the double render exposes the outside mutation.)
Code & diagrams
This example uses `<StrictMode>` on purpose. The render is logged twice, and the effect runs setup → cleanup → setup. Because the cleanup undoes the setup, the app ends with exactly one connection, as in production.
Preview
Console
Nothing logged yet.Expected console output
render ChatRoom
render ChatRoom
connect to general | open connections: 1
disconnect from general | open connections: 0
connect to general | open connections: 1Uses `<StrictMode>` on purpose. Two connections stay open. In production this bug appears later, every time the user leaves and re-enters the room; StrictMode shows it on the first load.
Preview
Console
Nothing logged yet.Expected console output
connect to general | open connections: 1
connect to general | open connections: 2Uses `<StrictMode>` on purpose. The ref keeps its value through the simulated remount, so the second setup returns early, but the first run's cleanup already disconnected. The room ends up with no connection at all. Delete the two ref lines: the plain effect with cleanup is the fix.
Preview
Console
Nothing logged yet.Expected console output
connect to general | open connections: 1
disconnect from general | open connections: 0
after mount, open connections: 0Uses `<StrictMode>` on purpose. Two requests start in development; the first run's cleanup set its `ignore` to true, so only the second response is used. In production there is one request. Data libraries also de-duplicate the request (Phase 14).
Preview
Console
Nothing logged yet.Expected console output
request 1 started for user 7
request 2 started for user 7
ignored the response from the first run
showing AshaBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Remove the ignore flag
In the fetching example, delete the if (ignore) { ... } block.
Myth vs fact
Myth
StrictMode makes effects run twice in production.
Fact
Development only. Production builds run each effect setup once per mount, with or without <StrictMode>.
Myth
Running effects twice is a React bug you should turn off.
Fact
It's a deliberate check that your effect can be cleaned up and re-run, which happens for real in production.
Myth
A ref flag is the standard fix for double effects.
Fact
It hides the check and often creates a worse bug (the cleanup ran, the setup didn't). Add the missing cleanup instead.
Myth
StrictMode remounts with fresh state.
Fact
The simulated remount keeps the same state and refs; only effects (and ref callbacks) are cleaned up and set up again.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
StrictMode's double render happens in the render phase: React calls the component twice and discards one result. React 17 silenced the console during the second call; React 18 stopped silencing (DevTools can dim it); in React 19 both calls print in the console.
- ▸
In React 19 the StrictMode re-render reuses the results of
useMemoanduseCallbackfrom the first call, and StrictMode also double-invokes ref callbacks on mount (attach → cleanup → attach), checking that ref cleanups are symmetric too. - ▸
The effect check is a preview of reusable state: features like
<Activity>(React 19.2) hide a subtree, destroying its effects while keeping its state, then re-create the effects when shown. Effects that survive StrictMode survive this. - ▸
You can wrap only part of the tree in
<StrictMode>to adopt it gradually in an old codebase; the checks apply to that subtree only.
Remember this
- 1
StrictMode is a component,
<StrictMode>, that you wrap around your app (Vite and Next.js templates do this for you; Topic 0.4). It renders nothing itself. In development builds it turns on extra checks for everything inside it. In production builds it does nothing at all: every render and every effect runs once. - 2
Check one: double rendering. React calls each component function twice (and the functions you pass to
useState,useMemoanduseReducer) and keeps one result. A pure component gives the same JSX both times, so nothing changes. An impure one, which changes an outside variable or pushes into an array during render, shows its bug: things happen twice. Bothconsole.logcalls print. In React 19,useMemoanduseCallbackreuse the first call's result during this second render. - 3
Check two: an extra effect cycle. When a component first mounts, React runs every effect's setup, then immediately its cleanup, then the setup again: mount → unmount → mount. This simulates the component leaving and returning, which really happens in production: switching tabs with
<Activity>(Phase 15), Fast Refresh while you edit, navigating back. React 19 also runs ref callbacks twice in the same way (attach, cleanup, attach). - 4
What it catches: an effect without cleanup (two connections open, two intervals ticking, two listeners firing), a cleanup that doesn't fully undo the setup, and a fetch whose first response arrives after the component has moved on. If your app behaves the same after setup → cleanup → setup as after one setup, your effect is correct.
- 5
The wrong fix is to silence the check: a
useRefflag such asif (didRun.current) return;, or removing StrictMode. Refs keep their value through StrictMode's simulated remount, so the second setup returns early, but the cleanup from the first run has already closed the connection, so you end up with no connection. Even when silencing seems to work, the bug is still there for real remounts. Fix the effect instead: add the cleanup (disconnect, clear, remove, ignore or abort the request). - 6
A few things should genuinely run once per app load, not once per mount: initialising an analytics SDK, reading a token from storage. Those don't belong in a component effect at all. Put them at the top level of a module or guard them with a module-level variable (Topic 7.6 shows how).
Explain it without notes
What exactly does StrictMode do in development, and what does it do in production?
Why does React run effects setup → cleanup → setup in StrictMode? What bugs does it catch?
Why is a useRef guard the wrong fix for an effect that runs twice?
Your data fetch fires twice in development. Is that a problem? How do you make it correct?
Practice
This component ticks twice as fast in development. Fix it:
``jsx
function Clock() {
const [s, setS] = useState(0);
useEffect(() => {
setInterval(() => setS((x) => x + 1), 1000);
}, []);
return <p>{s}</p>;
}
Write a Subscriber that calls store.subscribe(fn) (which returns an unsubscribe function) and survives StrictMode.
Trade-offs
- ↔
StrictMode's double logs and double requests in development are noise, but they find real bugs before users do. Keep it on; filter the console instead.
Done when you can
I know StrictMode checks run in development only.
I can predict the mount → cleanup → mount logs.
I fix a doubled effect by adding cleanup, not by adding a ref guard.
I can make a fetch safe under StrictMode with an ignore flag.
I know where to put code that must run once per app load.