Core Concept: Two Phases of React Rendering
Render Phase
During this phase:
Virtual DOM changes are computed
Function Component body is executed
Pure computational operations with no side effects
Can be interrupted by React in Concurrent Mode
Commit Phase
During this phase:
Changes are applied to the real DOM
Effect Hooks are executed (
useEffect,useLayoutEffect)Cannot be interrupted — must execute completely
Visual Process Flow
┌─────────────────────────────────────────────────┐
│ Render Phase │
│ 1. Execute Function Component │
│ 2. Compute Virtual DOM diff │
│ 3. Prepare content to update │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ Commit Phase │
│ 1. DOM Mutation (update real DOM) │
│ 2. useLayoutEffect (synchronous execution) │
│ 3. Browser Paint (screen update) │
│ 4. useEffect (asynchronous execution) │
└─────────────────────────────────────────────────┘
Three Effect Hooks: Execution Timing Breakdown
Complete Execution Order
Component Render
↓
┌──────────────┐
│ DOM Update │ ← Update real DOM
└──────────────┘
↓
┌────────────────────┐
│ useLayoutEffect │ ← Synchronous, BLOCKS paint
│ (Synchronous) │
└────────────────────┘
↓
┌──────────────┐
│ Browser Paint│ ← User sees updated screen
└──────────────┘
↓
┌────────────────────┐
│ useEffect │ ← Asynchronous, doesn't affect paint
│ (Asynchronous) │
└────────────────────┘
1. useEffect — For Most Scenarios
Execution Timing: After browser paint completes
Characteristic: Asynchronous execution, doesn't block rendering
Usage Frequency: ~99% of cases
useEffect(() => {
// Executes after browser paint
// Does not block screen rendering
}, [dependencies]);
Use Cases:
Data fetching (API calls)
Event subscription
Setting up timers
Analytics tracking
Code Example:
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
})
.catch(err => {
console.error(err);
setLoading(false);
});
}, [userId]);
if (loading) return <div>Loading...</div>;
return <div>{user?.name}</div>;
}
2. useLayoutEffect — Synchronous DOM Measurement & Adjustment
Execution Timing: After DOM update, before paint, executes synchronously
Characteristic: Blocks screen rendering until Hook completes
Usage: When synchronous DOM measurement or modification is needed
useLayoutEffect(() => {
// Executes after DOM update, before paint (synchronous)
// BLOCKS screen rendering
}, [dependencies]);
Use Cases:
Measuring DOM element dimensions (width, height, scrollHeight)
Adjusting DOM based on measurements
Preventing UI flicker
Dynamic tooltip or popover positioning
Code Example:
interface TooltipProps {
targetRef: React.RefObject<HTMLElement>;
children: React.ReactNode;
}
function Tooltip({ targetRef, children }: TooltipProps) {
const tooltipRef = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState({ top: 0, left: 0 });
useLayoutEffect(() => {
if (targetRef.current && tooltipRef.current) {
const targetRect = targetRef.current.getBoundingClientRect();
const tooltipRect = tooltipRef.current.getBoundingClientRect();
setPosition({
top: targetRect.bottom + 8,
left: targetRect.left + (targetRect.width - tooltipRect.width) / 2,
});
}
}, [targetRef]);
return (
<div
ref={tooltipRef}
style={{
position: 'fixed',
top: position.top,
left: position.left,
}}
>
{children}
</div>
);
}
3. useInsertionEffect — CSS-in-JS Specific (React 18+)
Execution Timing: Before any DOM mutation occurs
Target Users: CSS-in-JS libraries (emotion, styled-components, etc.)
General Developers: Rarely need this
useInsertionEffect(() => {
// Executes before any DOM mutation
// Exclusively for CSS-in-JS libraries
}, [dependencies]);
Execution Order:
useInsertionEffect → DOM Update → useLayoutEffect → Paint → useEffect
Practical Comparison: useEffect vs useLayoutEffect
The Flickering Problem
Problem with useEffect:
function FlickeringComponent() {
const [height, setHeight] = useState(0);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
// ❌ Executes after paint
// User first sees height=0, then correct height
if (ref.current) {
setHeight(ref.current.scrollHeight);
}
}, []);
return (
<div>
<div ref={ref} style={{ height }}>
Content with dynamic height
</div>
</div>
);
}
Solution with useLayoutEffect:
function NoFlickerComponent() {
const [height, setHeight] = useState(0);
const ref = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
// ✅ Executes before paint
// User only sees final correct state
if (ref.current) {
setHeight(ref.current.scrollHeight);
}
}, []);
return (
<div>
<div ref={ref} style={{ height }}>
Content with dynamic height
</div>
</div>
);
}
Execution Timing Verification
function TimingDemo() {
console.log('1. Component render');
useInsertionEffect(() => {
console.log('2. useInsertionEffect');
});
useLayoutEffect(() => {
console.log('4. useLayoutEffect');
});
useEffect(() => {
console.log('5. useEffect');
});
return <div ref={() => console.log('3. DOM ref callback')}>Content</div>;
}
// Expected console output order:
// 1. Component render
// 2. useInsertionEffect
// 3. DOM ref callback
// 4. useLayoutEffect
// 5. useEffect
useState Batching Mechanism
React 18 Automatic Batching
React 18 automatically batches multiple setState calls into a single re-render by default.
function BatchingDemo() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
const [text, setText] = useState('');
console.log('Render!'); // Only executes once
const handleClick = () => {
// React 18: These three updates are batched, triggers only one re-render
setCount(c => c + 1);
setFlag(f => !f);
setText('Updated');
};
// Even inside setTimeout, batching applies (React 18 feature)
const handleAsync = () => {
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React 18: Still triggers only one re-render
// React 17: Would trigger two re-renders
}, 0);
};
return (
<div>
<p>Count: {count}, Flag: {String(flag)}, Text: {text}</p>
<button onClick={handleClick}>Sync Update</button>
<button onClick={handleAsync}>Async Update</button>
</div>
);
}
Functional Updates vs Direct Updates
Wrong Approach — Sequential Direct Updates:
const incrementWrong = () => {
setCount(count + 1); // count is 0, set to 1
setCount(count + 1); // count still 0 (closure), set to 1
setCount(count + 1); // count still 0 (closure), set to 1
// Final result: 1 (expected 3)
};
Correct Approach — Functional Updates:
const incrementRight = () => {
setCount(c => c + 1); // 0 + 1 = 1
setCount(c => c + 1); // 1 + 1 = 2
setCount(c => c + 1); // 2 + 1 = 3
// Final result: 3 ✓
};
Force Synchronous Update
Some scenarios require immediate DOM updates:
import { flushSync } from 'react-dom';
function FlushSyncDemo() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
const handleClick = () => {
flushSync(() => {
setCount(c => c + 1);
});
// DOM is updated
flushSync(() => {
setFlag(f => !f);
});
// DOM is updated again
// Total: 2 re-renders (instead of default 1)
};
return <div>{count} - {String(flag)}</div>;
}
Class Component Lifecycle Mapping
Class Component | Function Component | Description |
|---|---|---|
|
| Initialize state |
| Compute in render or | Calculate based on props |
|
| Determine if re-render |
| Function Component body | Define UI structure |
|
| Execute after first mount |
|
| Execute after update |
|
| Cleanup before unmount |
|
| Capture state before DOM |
| Error Boundary (class needed) | Error handling |
Key Differences
// Class Component — Manual prop comparison
class ClassExample extends React.Component {
componentDidMount() {
// Executes once after initial mount
}
componentDidUpdate(prevProps) {
// Executes after every update
if (prevProps.id !== this.props.id) {
// Manual prop change detection
}
}
}
// Function Component — Automatic dependency tracking
function FunctionExample({ id }: { id: string }) {
useEffect(() => {
// Executes on initial mount + whenever id changes
// Dependency automatically tracked
}, [id]);
useEffect(() => {
// Executes only on initial mount
}, []);
useEffect(() => {
// Executes after every render (usually not recommended)
});
}
Real-World Examples
Example 1: Auto-Resizing Textarea
function AutoResizeTextarea() {
const textareaRef = useRef<HTMLTextAreaElement>(null);
const [value, setValue] = useState('');
useLayoutEffect(() => {
const textarea = textareaRef.current;
if (textarea) {
textarea.style.height = 'auto';
textarea.style.height = `${textarea.scrollHeight}px`;
}
}, [value]);
return (
<textarea
ref={textareaRef}
value={value}
onChange={(e) => setValue(e.target.value)}
style={{ overflow: 'hidden', resize: 'none' }}
/>
);
}
Example 2: Animated List Items
function AnimatedList({ items }: { items: string[] }) {
const [prevItems, setPrevItems] = useState<string[]>([]);
const [animatingItems, setAnimatingItems] = useState<Set<string>>(new Set());
useLayoutEffect(() => {
const newItems = items.filter(item => !prevItems.includes(item));
if (newItems.length > 0) {
setAnimatingItems(new Set(newItems));
requestAnimationFrame(() => {
requestAnimationFrame(() => {
setAnimatingItems(new Set());
});
});
}
setPrevItems(items);
}, [items, prevItems]);
return (
<ul>
{items.map(item => (
<li
key={item}
className={animatingItems.has(item) ? 'animate-in' : ''}
>
{item}
</li>
))}
</ul>
);
}
Example 3: Modal Focus Management
function Modal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
const modalRef = useRef<HTMLDivElement>(null);
const previousActiveElement = useRef<Element | null>(null);
useLayoutEffect(() => {
if (isOpen) {
previousActiveElement.current = document.activeElement;
modalRef.current?.focus();
} else {
(previousActiveElement.current as HTMLElement)?.focus();
}
}, [isOpen]);
if (!isOpen) return null;
return (
<div
ref={modalRef}
tabIndex={-1}
role="dialog"
aria-modal="true"
>
<h2>Modal Title</h2>
<button onClick={onClose}>Close</button>
</div>
);
}
Performance Considerations & Best Practices
Hook Impact on Paint
Hook | Blocks Paint | Performance Impact | Use Case |
|---|---|---|---|
| Yes | Severe | CSS-in-JS libraries |
| Yes | Moderate | DOM measurement, prevent flicker |
| No | Minimal | Most side effects |
Best Practices
// ✓ Good — Use useEffect for most scenarios
useEffect(() => {
fetchData();
subscribeToEvents();
}, []);
// ✓ Good — Use useLayoutEffect for synchronous DOM operations
useLayoutEffect(() => {
measureElement();
adjustPosition();
}, []);
// ✗ Bad — Time-consuming operations in useLayoutEffect
useLayoutEffect(() => {
// This blocks paint and causes UI lag!
heavyCalculation();
fetchData();
}, []);
// ✗ Bad — Missing dependency array
useEffect(() => {
// Executes after every render, causing performance issues
setupListener();
return () => removeListener();
});
// ✓ Good — Explicit dependency specification
useEffect(() => {
setupListener();
return () => removeListener();
}, [dependency]);
Common Interview Questions & Answers
Q1: What are the main differences between useEffect and useLayoutEffect?
Aspect | useEffect | useLayoutEffect |
|---|---|---|
Execution Timing | After paint (asynchronous) | Before paint (synchronous) |
Blocks Paint | No | Yes |
Usage Frequency | ~99% | ~1% |
Use Cases | API, subscriptions, timers | DOM measurement, prevent flicker |
Performance Impact | Minimal | Potential visual delay |
Q2: Why does calling setCount(count + 1) three times only increment by 1?
Reason: Closure causes all calls to receive the same count value.
// ❌ Wrong
const incrementWrong = () => {
setCount(count + 1); // 0 + 1 = 1
setCount(count + 1); // 0 + 1 = 1
setCount(count + 1); // 0 + 1 = 1
};
// ✓ Correct — Use functional updates
const incrementRight = () => {
setCount(c => c + 1); // 0 + 1 = 1
setCount(c => c + 1); // 1 + 1 = 2
setCount(c => c + 1); // 2 + 1 = 3
};
Q3: When does UI flickering occur? How do you fix it?
Problem: useEffect executes after paint, so user first sees old state, then new state.
Solution: Use useLayoutEffect to complete measurement and adjustment before paint.
// ❌ Flickers
useEffect(() => {
setPosition(measureElement());
}, []);
// ✓ No flicker
useLayoutEffect(() => {
setPosition(measureElement());
}, []);
Summary
Two Phases: Render Phase (pure computation) → Commit Phase (apply + effects)
useEffect: Executes after paint, suits ~99% of side effects
useLayoutEffect: Executes before paint, used for DOM measurement and preventing flicker
useInsertionEffect: Executes before DOM mutation, only for CSS-in-JS libraries
Batching: React 18 automatically batches multiple setState calls
Functional Updates: Use
setCount(c => c + 1)to ensure latest statePerformance: Prefer
useEffect, useuseLayoutEffectonly for synchronous operations
Mastering these concepts will significantly enhance your application's stability and performance.