Skip to content
Chapter 25Lesson 1

Strict Mode is the messenger

Your first look at React's effect lifecycle, through the Strict Mode checks that catch missing cleanups and impure code before they ship.

A bug report lands. A user opened a page, clicked away to another tab in your app, came back, and now the screen shows numbers from thirty seconds ago. Or they got two “Saved!” toasts instead of one. Or, once you reproduce it, your network panel shows the same request firing twice on every visit. Nothing in the code looks wrong. It worked on your machine. It worked in review. It works in production most of the time, which is the worst way for something to work.

The root cause is almost always the same. A component reached out to something outside React, such as an interval, a network request, or an event listener, and never tore it down when the component went away. The old one keeps running, a new one stacks on top, and the leak compounds every time the user navigates back.

React tried to tell you about this. The first time you loaded that page in development, React deliberately ran the component through a full mount, unmount, and mount again, precisely so a missing teardown would misbehave loudly, right there on your screen, weeks before any user saw it. That machinery is called Strict Mode . If you’ve ever been confused by “why is my effect running twice?”, you’ve already met it. You just read it as a nuisance instead of a free correctness test.

This lesson changes that reading. By the end, when you see something run twice in dev, you won’t reach for a switch to make it stop. You’ll read it as a signal that this code isn’t safe to run twice, and you’ll know the exact shape of the fix. You already hold the contract Strict Mode is checking: back in The purity contract, you learned that rendering is a pure function of props and state. Strict Mode is one of the ways React verifies you kept that promise. It also previews a second discipline, cleanup, that you’ll learn to write properly in the next lesson. For now the goal is narrower and more valuable: learn to trust the messenger.

Strict Mode is a component. You wrap part of your tree in it, and everything inside opts into a set of development-only checks.

src/index.tsx
<StrictMode>
<App />
</StrictMode>

It wraps a subtree, so in theory you could scope it to one risky corner of your app. In practice nobody does. You wrap the whole thing once at the root and forget about it.

In fact you don’t even write that wrapper yourself. If you scaffolded your project with Next.js, which every project in this course does, Strict Mode is already on. Next.js has wrapped your application in it by default since version 13.5.1, so your Next.js 16 app has been running every Client Component through these checks since the day you created it. There is a reactStrictMode flag in next.config you could flip off, but it defaults on. Turning it off to silence a warning is a mistake, and we’ll come back to why at the end of the lesson.

You can leave it on without a second thought because of the guarantee that makes the whole feature work: every one of these checks is development-only and stripped from your production build. No double renders ship, and no extra effect cycles run for real users. Because there is zero runtime cost in production, there is no performance argument for disabling it. Turning it off buys you nothing except the right to ship the bug it would have caught.

Strict Mode doesn’t run everything twice. It runs two specific categories of code twice, and they exist for two different reasons. Keeping the two reasons apart is what tells you which kind of problem a given doubling is pointing at.

The first category is things that are supposed to be pure. That covers your component function body itself; the functions you hand to useState, useReducer, and useMemo as initializers, including the useState(() => …) lazy initializer you met in The useState surface and lazy initialization; and the updater functions you pass to a setter, like setCount((c) => c + 1). Strict Mode runs each of these twice in dev. The logic is simple: if a function is genuinely pure, running it a second time produces the exact same result as the first, so the second run leaves no trace and you’d never know it happened. You only notice when the function isn’t pure, when it secretly changes something in the outside world. Then the second run makes that change a second time, and the damage shows. The doubling doesn’t break pure code; it exposes code that was never pure to begin with.

The second category is effects, and here the goal is different. Strict Mode isn’t checking purity; it’s checking cleanup. On the first mount in dev, instead of just running an effect’s setup once, React runs the full lifecycle: setup, then cleanup, then setup again. The next section unpacks exactly what that does. React 19 extends the same treatment to callback refs : the function-form ref you saw in useRef as the non-rendering escape hatch gets an extra call-and-cleanup cycle too. The idea is the same in both cases. Anything that sets something up gets run through a teardown to prove the teardown exists and works.

One detail clears up the most common confusion. Strict Mode does not double-invoke event handlers, and it does not double-invoke setTimeout or setInterval callbacks. A click that runs an onClick once still runs it once. Only render-time code and the effect-and-ref lifecycle get doubled. This is why a click handler that fires a request once behaves perfectly while an effect that fires the same request runs it twice: the handler was never in the double-invocation set in the first place.

The following exercise gives you a pool of code locations. Drop each into the bucket that matches what Strict Mode does with it in development.

Sort each piece of code by what Strict Mode does with it in development. Drag each item into the bucket it belongs to, then press Check.

Runs twice in dev Pure code or effect/ref lifecycle
Runs once in dev Not in the double-invocation set
The component function body
A useState(() => …) lazy initializer
A setCount((c) => c + 1) updater
A useMemo factory function
An effect’s setup function
A callback ref passed to ref={}
An onClick handler
The body of a setInterval callback
A setTimeout callback

This section is worth reading slowly, because it sets up everything the next lesson builds on.

You’ll meet the full effect API in the next lesson. For now, treat an effect as a black box with two parts. It has a setup, the code that runs after the component appears on screen, and an optional cleanup , a function the setup returns that React runs on the way out to undo whatever the setup did. You write useEffect(setup, []) and think of it this way: setup runs once when the component shows up, and cleanup runs once when it leaves. That’s the entire mental model you need today. Dependency arrays, the full signature, and the way effects re-synchronize when inputs change are all the next lesson’s job.

Here’s the twist Strict Mode adds. In development, on the very first mount, React doesn’t just run setup. It runs setup, then cleanup, then setup: a complete mount, unmount, and remount, compressed into the initial render. It’s simulating a user who shows up, leaves, and comes back, all in a fraction of a second, before you’ve even interacted with the page.

Now watch what that does to an effect that subscribes to something but forgets to unsubscribe. Say the setup starts an interval that logs a tick every second, and you didn’t write a cleanup. Scrub through the cycle below to see what happens.

1setup cleanup setup
runs const id = setInterval(tick, 1000)
Live timers 1 ticking
timer A every 1000ms no cleanup
Setup #1 runs. setInterval registers a timer. Live timers: 1 — but nothing yet guards it, because no cleanup was written.
1setup 2cleanup setup
nothing runs return () => clearInterval(id)
Live timers 1 still ticking
timer A every 1000ms still live
clearInterval(id) where the fix goes
Cleanup runs. But there is no cleanup written, so nothing is torn down — timer A is still alive. With a cleanup, this is exactly where clearInterval would have fired.
1setup cleanup 2setup
runs again const id = setInterval(tick, 1000)
Live timers 2 ticking in parallel
timer A every 1000ms still live
timer B every 1000ms new, stacked
Setup #2 runs. A second setInterval registers. Live timers: 2 — two intervals now tick in parallel.
setup cleanup setup !result
each tick console.log('tick')
Console — one second per second

ticktimer A

ticktimer B

The doubling is the symptom you see; the leak is the disease underneath.

The result: the tick logs twice per second. The doubling is the symptom you see; the leak is the disease underneath. A guard that hid the second tick would still leave one timer that never stops.

The cleanup isn’t decoration; it’s the thing that makes the second setup safe. Run the same effect with a cleanup that clears the interval, and the cleanup step actually fires. It clears timer #1 before setup #2 ever runs, so you’re left with exactly one live timer no matter how many times the component mounts. Here are the two shapes side by side.

useEffect(() => {
const id = setInterval(tick, 1000);
}, []);

Leaks, no cleanup. Strict Mode’s second setup stacks a second interval on top of the first, which was never cleared, so the tick fires twice.

An interval is the easiest leak to see, but it’s one of three you’ll meet constantly, and they all share the same shape: a setup reaches outside React to start something, and no cleanup ever stops it. Keep these three as a checklist for naming what kind of leak you’re looking at:

  • An interval or timeout that’s never cleared. It fires twice, as you just saw.
  • A network request that’s never aborted. Two requests go out in parallel, and whichever resolves last wins, which is how you get stale data on screen.
  • An event listener that’s never removed. The handler runs twice for every single event, because two copies are attached.

The next lesson codes all three properly. Today the point is recognition: when you see double, name which of these three it is, then write the cleanup. You don’t yet need to know the precise API to know the shape of the answer.

That’s the cleanup half of Strict Mode. The other half, running pure code twice, catches a different class of bug, and it leans directly on the purity contract you already hold.

The mechanism is the same logic from the effect side, pointed at render instead. If your render or your initializer does something observable to the outside world, such as pushing onto an array that lives at module scope, bumping a counter, writing to localStorage, or pinging an analytics endpoint, then running it twice does that thing twice. The array has duplicate entries. The counter reads 2 when you expected 1. The stored value is corrupted. Strict Mode didn’t cause any of that. It revealed an impurity that was always there, waiting to misbehave the moment React re-rendered the component, and React re-renders components constantly. The only thing Strict Mode changes is the timing: the misbehavior shows up now, on your screen, instead of intermittently in production when some unrelated state change happens to trigger a re-render.

The clearest version of this is the lazy initializer you just learned. useState(() => expensiveCompute()) runs that initializer twice in dev. If expensiveCompute is pure, reading its inputs, doing math, and returning a value, twice is identical to once and you never notice. But if it has a side effect tucked inside, a localStorage write or an analytics event or a counter bump, that side effect now fires twice. The same is true of the init argument you pass to useReducer. The fix is not to stop using lazy initialization: lazy init is correct and you should keep it. The fix is to keep the initializer pure. A side effect doesn’t belong in an initializer at all; it belongs in an effect or an event handler, where it runs when it’s supposed to.

The exercise below makes the doubling something you feel instead of something you’re told. Read the program, predict what it prints, then check yourself.

This component renders once, under Strict Mode, in development. Predict what this program prints, then press Check.

let renders = 0;
function Counter() {
const [value] = useState(() => {
renders++;
console.log('init', renders);
return 0;
});
return <p>{value}</p>;
}
// Rendered as <StrictMode><Counter /></StrictMode>

The same rule covers a side effect sitting directly in the render body, such as an analytics call written inline in the component. It fires twice in dev because the body runs twice. The fix is the one you’d reach for anyway: move it out of render into an effect or an event handler, because render must be pure. (A whole catalog of “this looks like it needs an effect but doesn’t” is coming a few lessons from now. For today the point is just that render is not the place for side effects.)

Once you see an effect run twice, one wrong move is especially tempting, partly because it feels clever.

Picture how it plays out. You don’t yet have the cleanup habit, so you reason: “I only want this to happen once, so let me guard it.” You reach for a ref to remember whether you’ve already run, and you bail out the second time. The guard lines below are the ones to distrust.

const didRun = useRef(false);
useEffect(() => {
if (didRun.current) return;
didRun.current = true;
const id = setInterval(tick, 1000);
}, []);

It works. The double-firing stops. And it is exactly wrong.

In 2026 this is worse than hiding a dev-time signal, because the thing Strict Mode is simulating is no longer hypothetical. React 19’s concurrent features genuinely mount, unmount, and remount components in production, for real users, not just as a dev simulation. Transitions and prefetching do it. The Activity API does it explicitly: <Activity> hides a piece of UI and later brings it back, running cleanups when it hides and re-running setups when it returns. A guard that assumes setup runs exactly once, and never again, is already wrong about how production behaves. Strict Mode was never a dev fiction inventing a scenario that can’t happen. It is a preview of the mount, unmount, and remount that production does on its own.

That gives you the rule the whole lesson has been building toward:

Write cleanups that make the second mount safe, never guards that try to prevent it.

The correct response to “this runs twice” is always the same: add or fix the cleanup. Once the cleanup is right, the double-mount is harmless, and that is exactly what Strict Mode is checking and what production needs from your component. You’re not making Strict Mode happy; you’re making your component correct, and Strict Mode is how you find out you succeeded.

(<Activity> is something you’ll meet properly when we reach the App Router. For now you only need the one fact: production remounts components on its own, so your cleanups have to handle it.)

This time, write the fix yourself rather than reading it. The component below leaks: its effect starts something and never cleans it up. Fix it so it survives being remounted.

This effect subscribes to a 'tick' event on window but never unsubscribes, so a remount stacks a second listener and the count jumps by two per tick. Return a cleanup from the effect that removes the listener — fix the shape, don't add a one-time guard.

Preview
    Reveal the fix
    useEffect(() => {
    const onTick = () => setCount((c) => c + 1);
    window.addEventListener('tick', onTick);
    return () => window.removeEventListener('tick', onTick);
    }, []);

    The returned cleanup removes the exact listener the setup added, so a remount tears the old one down before adding a new one. There is never more than one live listener, no matter how many times the component mounts.

    Where the doubling does and doesn’t happen

    Section titled “Where the doubling does and doesn’t happen”

    There is one last detail, so the doubling never surprises you in the wrong place. Strict Mode’s double-invocation is a Client Component phenomenon. Your Next.js app is Server Components by default, and Server Components don’t run under it: they execute once per request, on the server, and that’s all. The checks kick in only when you cross into client code (the 'use client' boundary you’ll learn in the App Router) and below.

    You’ll also occasionally see other dev-only warnings in the console: React flagging a deprecated API, an unsafe legacy lifecycle, or a string ref from some older third-party library. You won’t write any of those yourself, but it helps to recognize the shape. A yellow warning naming a component or an API is React telling you, in dev, about something that will cause trouble later. It’s the same messenger with a different message.

    The official reference covers the full list of checks, and the “Keeping Components Pure” page is the best reinforcement of the contract Strict Mode exists to verify.