| |

React 103 ⚛️ Double Pass Rendering and Strict Mode Behavior

⏱️ Estimated read: 13 min | |

Strict Mode is a development-only wrapper that deliberately runs certain parts of React’s lifecycle more than once. It is not a bug, a performance problem, or something that happens in production. It is a diagnostic tool that surfaces code which violates React’s rules—impure renders, missing effect cleanups, and state that does not reset correctly. The two behaviors that surprise developers most are the double-invocation of component functions and the mount-unmount-remount cycle that effects undergo . Both exist for the same reason: to make bugs visible during development that would otherwise appear only under rare conditions in production.

Key point: Strict Mode does not run twice in production. The double-invocation and the extra setup-cleanup cycle exist only in development. Code that works correctly under Strict Mode works correctly in production; code that breaks under Strict Mode has a bug that production will eventually expose.


Why Strict Mode double-invokes

The purity problem. React’s rendering model assumes that component functions are pure—they compute output from input and do nothing else. If a component mutates a global variable, writes to the DOM, or modifies an array in place, the render is impure. In production, an impure render might work by accident because React happens to run it once. But under concurrent rendering, React may discard a render and run it again. The second run sees the mutated global or the modified array, and the output is wrong. Strict Mode double-invokes the component function to make the impurity visible immediately . A component that adds an item to a list on every render will show the item twice. A component that writes to the DOM will write twice. The bug is visible in development instead of intermittent in production.

The cleanup problem. Effects have a setup function and, optionally, a cleanup function. If the cleanup is missing, the effect leaks—it adds an event listener without removing it, opens a connection without closing it, or starts a timer without clearing it. In production, the leak accumulates silently. Strict Mode runs an extra setup-cleanup cycle on mount to make the missing cleanup visible . The connection count jumps to two, the event listener fires twice, the timer runs twice. The bug is obvious in development instead of mysterious in production.

The state reset problem. A component that uses a ref to track whether it is mounted may set the ref to false in the cleanup and never set it back to true in the setup. Under Strict Mode’s mount-unmount-remount cycle, the ref is false after the second mount, and every asynchronous callback that checks it skips its state update . The UI gets stuck in a loading state. The bug is visible immediately instead of appearing when the user navigates away and back.


a. The Render Phase Double-Invocation

Strict Mode calls the component function twice during render. The first call is the one whose output is used. The second call is a check: if the component is pure, the second call produces the same output and nothing observable changes. If the component is impure, the second call produces a different output or a side effect, and the difference is a bug .

function StoryTray({ stories }) {
  // ❌ Impure: mutates the prop array on every render
  stories.push({ id: 'create', label: 'Create Story' });

  return (
    <ul>
      {stories.map(s => <li key={s.id}>{s.label}</li>)}
    </ul>
  );
}

Under Strict Mode, this component renders twice. The first render adds “Create Story” to the array. The second render adds it again. The list shows two “Create Story” entries. Without Strict Mode, the bug might not appear until the component re-renders for an unrelated reason, and then the list would have an extra entry that the developer did not intend .

The fix is to copy the array before modifying it, so the original prop is never mutated.

function StoryTray({ stories }) {
  // ✅ Pure: creates a new array, does not mutate the prop
  const items = [...stories, { id: 'create', label: 'Create Story' }];
  return <ul>{items.map(s => <li key={s.id}>{s.label}</li>)}</ul>;
}

Strict Mode also double-invokes state updater functions and initializers passed to useState, useMemo, and useReducer . These functions must be pure for the same reason: React may call them more than once, and the result must be the same each time.


b. The Effect Mount-Unmount-Remount Cycle

Strict Mode simulates a full unmount and remount of every component on initial mount. The sequence is: setup runs, cleanup runs, setup runs again . The purpose is to verify that the cleanup function actually cleans up. If the effect opens a WebSocket, the cleanup must close it. If the effect adds an event listener, the cleanup must remove it. If the effect starts a timer, the cleanup must clear it.

function ChatRoom({ roomId }) {
  useEffect(() => {
    // ✅ Setup: opens a connection
    const connection = createConnection(roomId);
    connection.connect();

    // ✅ Cleanup: closes the connection
    return () => connection.disconnect();
  }, [roomId]);

  return <div>Chat: {roomId}</div>;
}

Under Strict Mode, the sequence is: connect, disconnect, connect. The connection count briefly goes to one, back to zero, and then to one. If the cleanup is missing, the sequence is: connect, connect. The count goes to two and stays there. The leak is visible in development .

The same rule applies to callback refs. Strict Mode runs an extra setup-cleanup cycle for every callback ref . A ref callback that adds an element to a list without removing it in the cleanup will grow the list on every mount. A ref callback with a proper cleanup will not.


c. What Strict Mode Does Not Do

Strict Mode does not run twice in production. The double-invocation and the extra effect cycle are development-only behaviors . A component that works correctly under Strict Mode works correctly in production, but it runs once instead of twice.

Strict Mode does not affect the commit phase. The double-invocation happens during the render phase. The commit phase still runs once, and the DOM is updated once . The user never sees the double render.

Strict Mode does not require you to write code that detects it. The correct approach is not to add a ref that tracks whether the effect has already run, or to check process.env.NODE_ENV inside the effect. Those workarounds make the code more complex and less correct . The correct approach is to write effects with proper cleanup and components that are pure. If the code is correct without Strict Mode, it is correct with Strict Mode. If it breaks with Strict Mode, it has a bug.

The common workaround of removing <StrictMode> from the entry point is a mistake. It disables the diagnostic without fixing the bug. The bug will still exist in production, and it will appear when the conditions are right—under concurrent rendering, under fast navigation, or under a state update that interrupts a render .


Complete Example Session

// ============================================
// PART 1: IMPURE RENDER — DOUBLE-INVOKED
// ============================================
function Counter() {
  let count = 0;
  // ❌ Impure: mutates a local on every render
  count++;
  return <div>{count}</div>;
}
// Strict Mode: renders twice, count is 2
// Production: renders once, count is 1
// The bug is invisible in production until a re-render


// ============================================
// PART 2: PURE RENDER — DOUBLE-INVOKED SAFELY
// ============================================
function Counter() {
  const [count, setCount] = useState(0);
  // ✅ Pure: computes from state, no mutation
  return <div>{count}</div>;
}
// Strict Mode: renders twice, output is identical
// Production: renders once, output is identical


// ============================================
// PART 3: EFFECT WITHOUT CLEANUP — LEAK VISIBLE
// ============================================
function Chat() {
  useEffect(() => {
    const conn = createConnection();
    conn.connect();
    // ❌ Missing cleanup: connection is never closed
  }, []);
  return <div>Chat</div>;
}
// Strict Mode: setup runs twice
// Two connections are open
// The leak is visible immediately


// ============================================
// PART 4: EFFECT WITH CLEANUP — LEAK FIXED
// ============================================
function Chat() {
  useEffect(() => {
    const conn = createConnection();
    conn.connect();
    // ✅ Cleanup closes the connection
    return () => conn.disconnect();
  }, []);
  return <div>Chat</div>;
}
// Strict Mode: connect, disconnect, connect
// One connection is open at any time
// The leak is gone


// ============================================
// PART 5: CALLBACK REF WITHOUT CLEANUP
// ============================================
function List() {
  const items = [];
  const ref = (el) => {
    if (el) items.push(el);
    // ❌ Missing cleanup: items grow on every mount
  };
  return <div ref={ref} />;
}
// Strict Mode: ref setup runs twice
// items has two elements
// The leak is visible immediately


// ============================================
// PART 6: CALLBACK REF WITH CLEANUP
// ============================================
function List() {
  const items = [];
  const ref = (el) => {
    if (el) items.push(el);
    // ✅ Cleanup removes the element
    return () => {
      const index = items.indexOf(el);
      if (index > -1) items.splice(index, 1);
    };
  };
  return <div ref={ref} />;
}
// Strict Mode: setup, cleanup, setup
// items has one element
// The leak is gone


// ============================================
// PART 7: STATE UPDATER PURITY
// ============================================
function Component() {
  const [count, setCount] = useState(0);
  // ❌ Impure updater: depends on external variable
  setCount(count + Math.random());
  // ✅ Pure updater: depends only on previous state
  setCount(c => c + 1);
  return null;
}
// Strict Mode: updater runs twice
// Impure updater produces different results


// ============================================
// PART 8: REF GUARD BUG
// ============================================
function useIsMounted() {
  const mounted = useRef(true);
  useEffect(() => {
    // ❌ Missing: mounted.current = true
    return () => { mounted.current = false; };
  }, []);
  return mounted;
}
// Strict Mode: cleanup sets mounted to false
// Second mount does not reset it to true
// Every async callback sees false
// FIX: set mounted.current = true in the effect body


// ============================================
// PART 9: REMOVING STRICTMODE IS NOT THE FIX
// ============================================
// Removing <StrictMode> disables the diagnostic
// The bug still exists in production
// It appears under concurrent rendering, fast navigation,
// or a state update that interrupts a render


// ============================================
// PART 10: PRODUCTION BEHAVIOR
// ============================================
// Render phase: runs once
// Effect setup: runs once
// Effect cleanup: runs on unmount only
// The double-invocation is development-only

The ten parts trace the double-invocation, the effect cycle, and the bugs that Strict Mode surfaces. Part 3 shows the leak that Strict Mode makes visible. Part 4 shows the cleanup that fixes it. Part 8 shows the ref guard bug that is easy to miss because the cleanup is correct but the setup does not reset the ref.


Quick Reference

Strict Mode Behaviors

BehaviorDevelopmentProduction
Component functionCalled twiceCalled once
State updaterCalled twiceCalled once
useState initializerCalled twiceCalled once
Effect setupRuns twice (mount)Runs once
Effect cleanupRuns twice (mount)Runs on unmount
Callback refSetup twiceSetup once

What Strict Mode Surfaces

BugSymptom
Impure renderOutput differs on second render
Missing effect cleanupConnection/listener/timer count grows
Missing ref cleanupList grows, listeners duplicate
State updater impurityDifferent result on second call
Ref guard not resetAsync callbacks skip updates

Common Fixes

BugFix
Mutating props/state in renderCopy before modifying
Missing cleanupReturn cleanup function from effect
Missing ref cleanupReturn cleanup from ref callback
Ref guard stuck falseSet ref to true in effect body
Impure state updaterUse only previous state

Best Practices

✅ Do This:

// Keep render pure
const items = [...stories, newItem];                                  // ✅
// Return cleanup from every effect that acquires a resource
useEffect(() => { const sub = subscribe(); return () => sub.unsubscribe(); }, []); // ✅
// Return cleanup from callback refs
const ref = (el) => { if (el) list.push(el); return () => list.splice(list.indexOf(el), 1); }; // ✅
// Reset ref guards in the effect body
useEffect(() => { mounted.current = true; return () => { mounted.current = false; }; }, []); // ✅

❌ Don’t Do This:

// Mutate props or state during render
stories.push(newItem);                                                // ❌
// Omit cleanup for subscriptions, timers, or connections
useEffect(() => { const conn = connect(); }, []);                      // ❌
// Remove StrictMode to make the symptoms disappear
// The bug remains in production                                    // ❌
// Add a ref guard that skips the second effect run
const ran = useRef(false); if (ran.current) return; ran.current = true; // ❌

Common Pitfalls

PitfallWhy It HappensFix
Effect runs twiceStrict Mode mount-unmount-remountAdd cleanup; do not skip
Ref is false after mountCleanup sets it false, setup does not resetSet true in effect body
List grows on mountCallback ref has no cleanupReturn cleanup from ref
State updater produces different resultUpdater depends on external valueUse only previous state
Production bug after removing StrictModeBug was masked, not fixedFix the code; keep StrictMode

Real-World Examples

1. Impure Array Mutation

stories.push({ id: 'create', label: 'Create Story' }); // ❌
const items = [...stories, { id: 'create', label: 'Create Story' }]; // ✅

2. Effect Without Cleanup

useEffect(() => { const conn = connect(); }, []); // ❌
useEffect(() => { const conn = connect(); return () => conn.disconnect(); }, []); // ✅

3. Callback Ref Without Cleanup

const ref = (el) => { if (el) list.push(el); }; // ❌
const ref = (el) => { if (el) list.push(el); return () => list.splice(list.indexOf(el), 1); }; // ✅

4. Ref Guard Not Reset

useEffect(() => { return () => { mounted.current = false; }; }, []); // ❌
useEffect(() => { mounted.current = true; return () => { mounted.current = false; }; }, []); // ✅

5. Impure State Updater

setCount(count + Math.random()); // ❌
setCount(c => c + 1); // ✅

6. Strict Mode Wrapper

<StrictMode><App /></StrictMode>

7. Production Build

npm run build  # Strict Mode double-invocation does not run

8. DevTools Console Dimming

Second render logs appear dimmed in React DevTools

9. Removing StrictMode

// Do not do this to hide symptoms
<App />  // StrictMode removed

10. Effect Debugging

useEffect(() => { console.log('setup'); return () => console.log('cleanup'); }, []);
// Strict Mode: setup, cleanup, setup

Visual

The Strict Mode Cycle

┌──────────────────────────────────────────────────────────────┐
│  DEVELOPMENT (Strict Mode)                                   │
│                                                              │
│  ┌─────────────────┐                                        │
│  │ Render 1        │  Component function called             │
│  └────────┬────────┘                                        │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │ Render 2        │  Called again (check purity)           │
│  └────────┬────────┘                                        │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │ Commit          │  DOM updated once                      │
│  └────────┬────────┘                                        │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │ Effect setup    │  First run                             │
│  └────────┬────────┘                                        │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │ Effect cleanup  │  Simulated unmount                     │
│  └────────┬────────┘                                        │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │ Effect setup    │  Second run                            │
│  └─────────────────┘                                        │
│                                                              │
│  In production: render once, setup once, cleanup on unmount. │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Missing Cleanup: Visible Leak

┌──────────────────────────────────────────────────────────────┐
│  EFFECT WITHOUT CLEANUP                                      │
│                                                              │
│  ┌─────────────────┐                                        │
│  │ Setup 1         │  Creates connection                    │
│  │                 │  Active: 1                             │
│  └────────┬────────┘                                        │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │ (No cleanup)    │  Connection is not closed              │
│  │                 │  Active: 1                             │
│  └────────┬────────┘                                        │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │ Setup 2         │  Creates another connection            │
│  │                 │  Active: 2                             │
│  └─────────────────┘                                        │
│                                                              │
│  The leak is visible in development.                         │
│  Without Strict Mode, the leak would be silent.              │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Proper Cleanup: No Leak

┌──────────────────────────────────────────────────────────────┐
│  EFFECT WITH CLEANUP                                         │
│                                                              │
│  ┌─────────────────┐                                        │
│  │ Setup 1         │  Creates connection                    │
│  │                 │  Active: 1                             │
│  └────────┬────────┘                                        │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │ Cleanup 1       │  Closes connection                     │
│  │                 │  Active: 0                             │
│  └────────┬────────┘                                        │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │ Setup 2         │  Creates connection                    │
│  │                 │  Active: 1                             │
│  └─────────────────┘                                        │
│                                                              │
│  The active count never exceeds one.                         │
│  The cleanup works correctly.                                │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Ref Guard Bug

┌──────────────────────────────────────────────────────────────┐
│  REF GUARD NOT RESET                                         │
│                                                              │
│  ┌─────────────────┐                                        │
│  │ Mount 1         │  mounted.current = true                │
│  └────────┬────────┘                                        │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │ Cleanup         │  mounted.current = false               │
│  └────────┬────────┘                                        │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │ Mount 2         │  Setup runs                            │
│  │                 │  But mounted.current is still false    │
│  └────────┬────────┘                                        │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │ Async callback  │  Checks mounted.current                │
│  │                 │  Sees false, skips update              │
│  └─────────────────┘                                        │
│                                                              │
│  FIX: set mounted.current = true in the effect body.         │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
Strict ModeDevelopment-only diagnostic wrapper
Render double-invokeComponent function called twice
Effect cycleSetup, cleanup, setup on mount
Callback ref cycleSetup, cleanup, setup on mount
ProductionAll of the above run once
PurposeSurface impure renders, missing cleanups, state bugs
Common fixCleanup functions, pure renders, reset ref guards
Wrong fixRemove Strict Mode, skip second run with a ref

Key takeaways:

  • Strict Mode double-invokes the render phase to surface impurity. A component that mutates props, globals, or the DOM during render will produce a different result on the second call. The difference is the bug. A pure component produces the same result and nothing observable changes .
  • Strict Mode runs an extra setup-cleanup cycle to surface missing cleanup. An effect that opens a connection without closing it will leave two connections open. The leak is visible in development instead of silent in production .
  • The mount-unmount-remount cycle can expose ref guard bugs. A ref that is set to false in cleanup and not reset to true in setup will cause asynchronous callbacks to skip their updates after the second mount .
  • Removing Strict Mode is not a fix. It disables the diagnostic without fixing the bug. The bug remains in production and appears under concurrent rendering, fast navigation, or interrupted updates .
  • The correct approach is to write code that works with Strict Mode. Pure renders, cleanup functions, and ref guards that reset correctly. Code that works correctly under Strict Mode works correctly in production.

Remember: Strict Mode is a smoke detector, not a fire. It does not create bugs; it reveals them. The double render, the double effect, and the remount cycle are all diagnostic behaviors that exist only in development. A component that breaks under Strict Mode has a bug that production will eventually expose under the right conditions. The fix is always in the component, not in the Strict Mode wrapper. The patterns that pass Strict Mode—pure renders, cleanup functions, reset ref guards—are the patterns that make concurrent rendering safe.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!