React 103 ⚛️ Double Pass Rendering and Strict Mode Behavior
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
| Behavior | Development | Production |
|---|---|---|
| Component function | Called twice | Called once |
| State updater | Called twice | Called once |
useState initializer | Called twice | Called once |
| Effect setup | Runs twice (mount) | Runs once |
| Effect cleanup | Runs twice (mount) | Runs on unmount |
| Callback ref | Setup twice | Setup once |
What Strict Mode Surfaces
| Bug | Symptom |
|---|---|
| Impure render | Output differs on second render |
| Missing effect cleanup | Connection/listener/timer count grows |
| Missing ref cleanup | List grows, listeners duplicate |
| State updater impurity | Different result on second call |
| Ref guard not reset | Async callbacks skip updates |
Common Fixes
| Bug | Fix |
|---|---|
| Mutating props/state in render | Copy before modifying |
| Missing cleanup | Return cleanup function from effect |
| Missing ref cleanup | Return cleanup from ref callback |
| Ref guard stuck false | Set ref to true in effect body |
| Impure state updater | Use 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Effect runs twice | Strict Mode mount-unmount-remount | Add cleanup; do not skip |
Ref is false after mount | Cleanup sets it false, setup does not reset | Set true in effect body |
| List grows on mount | Callback ref has no cleanup | Return cleanup from ref |
| State updater produces different result | Updater depends on external value | Use only previous state |
| Production bug after removing StrictMode | Bug was masked, not fixed | Fix 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
| Item | Value |
|---|---|
| Strict Mode | Development-only diagnostic wrapper |
| Render double-invoke | Component function called twice |
| Effect cycle | Setup, cleanup, setup on mount |
| Callback ref cycle | Setup, cleanup, setup on mount |
| Production | All of the above run once |
| Purpose | Surface impure renders, missing cleanups, state bugs |
| Common fix | Cleanup functions, pure renders, reset ref guards |
| Wrong fix | Remove 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
falsein cleanup and not reset totruein 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.
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!