Command Palette

Search for a command to run...

Hectal
PHASE 7Intermediate ~21 min· topic 4 of 9

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 true so 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.

main.jsxwhole filejsx
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.

On first mount, effects run three stepsdiagram
Rendering diagram…

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.

Fix the effect, don't silence itdiagram
Rendering diagram…

Try it yourself

  1. 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. 2

    Fix the leaking room

    In the second example, add return () => connection.disconnect();. Predict the final count before you run. (1.)

  3. 3

    Break the purity check

    In the first example, add const seen = []; at the top of the module and seen.push(roomId); inside ChatRoom's body, then log seen.length from a setTimeout. Predict the number. (2: the double render exposes the outside mutation.)

Code & diagrams

StrictMode: render twice, then connect, disconnect, connect New tab

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

Press Run to see the page.

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: 1
The bug StrictMode reveals: no cleanup New tab

Uses `<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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

connect to general | open connections: 1
connect to general | open connections: 2
Silencing with a ref makes it worse New tab

Uses `<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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

connect to general | open connections: 1
disconnect from general | open connections: 0
after mount, open connections: 0
Fetching under StrictMode: the ignore flag New tab

Uses `<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

Press Run to see the page.

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 Asha

Break 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.

terminal
$ Console
── what you'll see ──
request 1 started for user 7
request 2 started for user 7
showing Asha
showing Asha

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 useMemo and useCallback from 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. 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. 2

    Check one: double rendering. React calls each component function twice (and the functions you pass to useState, useMemo and useReducer) 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. Both console.log calls print. In React 19, useMemo and useCallback reuse the first call's result during this second render.

  3. 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. 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. 5

    The wrong fix is to silence the check: a useRef flag such as if (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. 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

01

What exactly does StrictMode do in development, and what does it do in production?

02

Why does React run effects setup → cleanup → setup in StrictMode? What bugs does it catch?

03

Why is a useRef guard the wrong fix for an effect that runs twice?

04

Your data fetch fires twice in development. Is that a problem? How do you make it correct?

Practice

01

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>; }

02

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.