How React Works (Part 5)? The React Lifecycle From the Inside: When Things Actually Run
Current Situation Analysis
Most React developers operate under an oversimplified mental model: useEffect is simply "code that runs after render." This misconception leads to predictable failure modes in production applications. When developers treat effects as immediate post-render callbacks, they frequently introduce visible UI flicker, memory leaks, and main-thread blocking.
The core pain points stem from three architectural realities that traditional mental models ignore:
- Phase Separation: Effects are fundamentally decoupled from the Render phase. React's execution pipeline strictly separates Render (tree diffing), Commit (DOM mutation), and Effects (side-effect execution). Treating them as a single synchronous flow causes timing bugs.
- Scheduler Abstraction: React does not execute
useEffectcallbacks immediately after the Commit phase. Instead, it queues them as separate tasks in the Scheduler. This deliberate decoupling prevents side effects from blocking the browser's paint cycle, but it breaks expectations for developers accustomed to synchronous lifecycle methods. - Layout vs. Passive Effects: Using
useEffectfor DOM measurements or layout adjustments forces the browser to paint an incorrect state first, then immediately correct it. This creates a visible layout shift (flicker) that degrades UX and harms performance metrics like CLS (Cumulative Layout Shift).
Traditional class-component lifecycles (componentDidMount, componentDidUpdate) ran synchronously during Commit, which made DOM measurements safe but caused jank. Modern React's dual-phase effect system solves this but requires precise understanding of macro-task scheduling and browser paint cycles to avoid subtle timing bugs.
WOW Moment: Key Findings
Experimental profiling of React's execution pipeline reveals a strict temporal boundary between layout mutations and passive side effects. The following data compares execution characteristics across common React effect patterns:
| Approach | Execution Phase | Browser Paint Timing | DOM Measurement Safety | Scheduler Queue Type |
|---|---|---|---|---|
useEffect (Passive) | Post-Commit | Asynchronous (after paint) | β Unsafe (causes layout flicker) | Macro Task (Scheduler) |
useLayoutEffect (Layout) | Synchronous Commit | Synchronous (before paint) | β Safe (prevents flicker) | Micro Task / Sync Block |
componentDidMount (Legacy) | Synchronous Commit | Synchronous (before paint) | β Safe | Sync Block (Class Fiber) |
Key Findings:
- Sweet Spot:
useLayoutEffectshould be strictly reserved for DOM measurements and synchronous layout corrections. All other side effects (subscriptions, API calls, logging) belong inuseEffectto preserve main-thread responsiveness. - Execution Order Guarantee: React guarantees
useLayoutEffectcallbacks fire synchronously in the Commit phase, whileuseEffectcallbacks are deferred to a new macro task. This explains whyrender β layout effect β effectis the only valid execution order. - Cleanup Determinism: Cleanup functions always execute before their corresponding new effect mounts (
commitPassiveUnmountEffectsβcommitPassiveMountEffects), ensuring no duplicate subscriptions or orphaned event listeners exist in the tree.
C
ore Solution React's effect lifecycle is governed by a three-phase pipeline: Render β Commit β Effects. Understanding the exact boundary between these phases is critical for predictable side-effect management.
1. The Three-Phase Pipeline
- Render: React executes component functions, diffs the virtual tree, and determines mutations. No DOM writes occur. This is where
console.log('render')fires. - Commit: React applies calculated mutations to the real DOM synchronously. This phase is uninterruptible.
- Effects: After DOM updates, React schedules passive effects. Layout effects run synchronously during Commit; passive effects run asynchronously after browser paint.
2. useEffect Execution Model
useEffect is not a direct callback. It is a declaration of deferred work. After Commit finishes, React queues the effect callbacks in the Scheduler's macro-task queue. The execution sequence is:
1. Render phase β your component functions run
2. Commit phase β DOM is updated
3. Browser paints the screen
4. Scheduler fires β useEffect callbacks run
Because steps 1-2 occupy one macro task and step 4 occupies a new one, the browser is guaranteed a paint opportunity between DOM mutation and effect execution.
3. Cleanup Lifecycle Determinism
React enforces strict cleanup ordering to prevent side-effect duplication:
useEffect(() => {
const subscription = subscribe(id);
return () => subscription.unsubscribe(); // cleanup
}, [id]);
When dependencies change or the component unmounts, React executes commitPassiveUnmountEffects (cleanup) before commitPassiveMountEffects (new effect). This runs in the same scheduled task, respecting tree traversal order (children before parents).
4. useLayoutEffect Synchronous Execution
useLayoutEffect bypasses the Scheduler and runs synchronously inside the Commit phase:
1. Render phase β component functions run
2. Commit phase β DOM is updated
3. useLayoutEffect callbacks run β here, synchronously, before paint
4. Browser paints
5. useEffect callbacks run β here, in next macro task
This is the only safe window for DOM measurements that affect layout:
// useLayoutEffect β correct for DOM measurements
useLayoutEffect(() => {
const rect = ref.current.getBoundingClientRect();
setTooltipPosition({ top: rect.bottom, left: rect.left });
});
// useEffect β would cause a visible flicker for measurements
// because the browser already painted before this runs
useEffect(() => {
const rect = ref.current.getBoundingClientRect();
setTooltipPosition({ top: rect.bottom, left: rect.left }); // too late
});
Pitfall Guide
- Using
useEffectfor Layout Calculations: Measuring DOM elements or adjusting layout insideuseEffectcauses a visible flicker because the browser paints the unadjusted state first. Always useuseLayoutEffectfor synchronous layout corrections. - Assuming Synchronous Post-Render Execution: Treating
useEffectas a direct continuation of the render function leads to race conditions. Effects are scheduled as macro tasks; state updates triggered inside them will queue a new render cycle, not update the current one. - Omitting or Misordering Cleanup Functions: Forgetting to return a cleanup function, or attempting to run setup logic before cleanup, causes memory leaks and duplicate subscriptions. React guarantees cleanup runs first (
commitPassiveUnmountEffects), but developers must explicitly define it. - Blocking the Commit Phase with Heavy Logic: Placing CPU-intensive operations inside
useLayoutEffectblocks the main thread and delays browser paint, causing jank. ReserveuseLayoutEffectstrictly for fast DOM reads/writes; offload heavy computation touseEffector Web Workers. - Relying on Cross-Component Effect Ordering: While React guarantees children run before parents within the same tree, effect execution order across independent component branches is not strictly deterministic. Design effects to be idempotent and independent of sibling execution timing.
Deliverables
- π Blueprint: React Effect Lifecycle Execution Map β A visual reference detailing the exact temporal boundaries between Render, Commit, Layout Effects, and Passive Effects, including macro-task scheduling boundaries and browser paint windows.
- β
Checklist: Effect Timing & Cleanup Verification β A 7-point validation checklist for code reviews: (1) DOM measurements use
useLayoutEffect, (2) API calls/subscriptions useuseEffect, (3) Cleanup functions are explicitly returned, (4) No heavy synchronous logic in layout effects, (5) Dependency arrays match actual closure variables, (6) Idempotent effect design, (7) SSR compatibility verified (useLayoutEffectfallback). - βοΈ Configuration Templates: Standardized Effect Setup/Cleanup Patterns β Production-ready boilerplate for common side effects (WebSocket subscriptions, IntersectionObserver, ResizeObserver, Timer management) with built-in cleanup guards, dependency validation, and SSR-safe wrappers.
