Skip to content
Chapter 25Lesson 1

What Strict Mode catches in development

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

A bug report lands. A user opened a page, switched to another tab in your app, came back, and the screen shows numbers from thirty seconds ago. Or they got two “Saved!” toasts instead of one. Or your network panel shows the same request firing twice on every visit. Nothing in the code looks wrong, and it works in production most of the time, which is the hardest kind of bug to catch.

The cause is almost always the same. A component reached out to something outside React, an interval, a network request, 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.

In development, the first time you loaded that page, React ran the component through a full mount, unmount, and mount again, precisely so a missing teardown would misbehave loudly on your screen, weeks before any user saw it. That machinery is Strict Mode . If you’ve ever wondered why an effect runs twice, you’ve met it, and read it as a nuisance rather than a free correctness test.

You already hold the contract it checks: in The purity contract, you learned that rendering is a pure function of props and state. Strict Mode is one way React verifies you kept that promise. So when something runs twice in dev, don’t reach for the switch that stops it. Read it as a signal that the code isn’t safe to run twice, and learn the shape of the fix.

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, but you almost always wrap the whole app once at the root.

You don’t even write that wrapper yourself. Next.js has enabled Strict Mode by default since version 13.5.1, so your Next.js 16 app has run every Client Component through these checks since the day you created it. A reactStrictMode flag in next.config turns it off, but leave it on; we’ll see at the end of the lesson why turning it off to silence a warning is a mistake.

The checks are stripped from your production build, so no double renders or extra effect cycles reach real users. With zero production cost, turning it off buys you nothing except the right to ship the bug it would have caught.

What Strict Mode doubles, and what it leaves alone

Section titled “What Strict Mode doubles, and what it leaves alone”

Strict Mode doesn’t run everything twice. It doubles two categories of code, each for a different reason, and telling the reasons apart tells you what a given doubling is pointing at.

The first category is code that’s supposed to be pure: your component function body; the initializers you hand to useState, useReducer, and useMemo, including the useState(() => …) lazy initializer from The useState surface; and the updater functions you pass to a setter, like setCount((c) => c + 1). A truly pure function returns the same result the second time and changes nothing outside itself, so the second run leaves no trace. You only notice the doubling when the function secretly mutates the outside world: now that change happens twice, and the bug surfaces. The second run doesn’t break pure code; it exposes code that was never pure.

The second category is effects, and here Strict Mode checks cleanup rather than purity. On the first mount in dev, React runs the full lifecycle instead of setup alone: setup, cleanup, then setup again. React 19 gives callback refs the same treatment, so the function-form ref from useRef gets an extra call-and-cleanup cycle too. Anything that sets something up is run through a teardown to prove the teardown works.

Strict Mode does not double event handlers, and it does not double setTimeout or setInterval callbacks. A click that runs an onClick once still runs it once. 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.

You’ll meet the full effect API next lesson. For now, treat an effect as a black box with two parts: a setup, the code that runs after the component appears on screen, and an optional cleanup that the setup returns. You write useEffect(setup, []): setup runs when the component shows up, cleanup runs when it leaves.

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 full mount, unmount, and remount compressed into the initial render, simulating a user who shows up, leaves, and comes back before you’ve even touched the page.

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, with no cleanup. Scrub through the cycle below.

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 is what makes the second setup safe. Add a cleanup that clears the interval, and the cleanup step fires between the two setups: it clears timer #1 before setup #2 runs, leaving 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. The second setup stacks a second interval on 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, all the same shape: a setup reaches outside React to start something, and no cleanup stops it. Use these three to name the 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 stale data lands on screen.
  • An event listener that’s never removed. Two copies are attached, so the handler runs twice for every event.

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

That covers the cleanup half of Strict Mode. The other half, running pure code twice, catches a different class of bug, and it builds on the purity contract you already know.

It is the same logic from the effect side, pointed at render. If your render or your initializer does something observable, pushing onto a module-scope array, bumping a counter, writing to localStorage, pinging an analytics endpoint, then running it twice does that thing twice. Strict Mode didn’t cause this; it revealed an impurity that was always there, waiting to misbehave the moment React re-rendered the component, and React re-renders constantly. All Strict Mode changes is the timing: the bug shows up now, on your screen, instead of intermittently in production when some unrelated state change triggers a re-render.

The lazy initializer you just learned is the clearest case. useState(() => expensiveCompute()) runs that initializer twice in dev, and so does the init argument to useReducer. If the initializer is pure, twice is identical to once and you never notice. If it hides a side effect, that side effect fires twice. The fix is not to drop lazy initialization, which is correct and worth keeping; it is to keep the initializer pure. A side effect belongs in an effect or an event handler, where it runs when it’s supposed to.

Read the program below, 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, like an analytics call written inline. It fires twice in dev because the body runs twice, and the fix is the same: move it into an effect or an event handler, because render must be pure. Many things that look like they need an effect don’t, which a later lesson covers; for now the point is just that render is no place for side effects.

When an effect runs twice, one wrong fix is tempting because it feels clever. You don’t yet have the cleanup habit, so you reason: “I only want this once, so let me guard it.” You reach for a ref that remembers whether you’ve already run, and bail out the second time. Distrust the lines below.

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 silencing a dev-time signal, because what Strict Mode simulates is no longer hypothetical. React 19’s concurrent features mount, unmount, and remount components in production, for real users. 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 is already wrong about how production behaves. Strict Mode isn’t inventing a scenario that can’t happen; it previews the remounting 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 it’s right, the double-mount is harmless, which is exactly what both Strict Mode and production need from your component. You’ll meet <Activity> properly at the App Router; for now, hold onto the one fact that motivates all of this: 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.

    Doubling happens only in Client Components

    Section titled “Doubling happens only in Client Components”

    Double-invocation is a Client Component behavior. Your Next.js app is Server Components by default, and those run once per request on the server, never doubled. The checks start only past the 'use client' boundary you’ll meet in the App Router, so if you don’t see doubling yet, you haven’t crossed into client code.

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

    The official reference lists every check; the “Keeping Components Pure” page reinforces the contract Strict Mode verifies.