The full journey from JSX to pixels β trigger a render and watch each phase execute.
Every time React renders, it goes through a predictable two-phase pipeline: the Render Phase and the Commit Phase. The render phase is pure and interruptible β React calls your components and builds a new fiber tree. The commit phase is synchronous and side-effectful β React applies the computed changes to the real DOM and fires effects.
At build time, JSX like <Button color='blue'>Click</Button> becomes React.createElement(Button, '{' color: 'blue' '}', 'Click'). This produces a plain object tree β the virtual DOM.
React walks the fiber tree calling each component function (or render() method). This phase is pure and produces no side effects β React can interrupt and restart it at any time in concurrent mode.
React compares the new output against the previous fiber tree. It marks each fiber with an effect tag: Update, Placement (insert), or Deletion. The result is an 'effect list' β a flat list of fibers with pending work.
React calls getSnapshotBeforeUpdate() on class components and schedules passive effects. This sub-phase runs before any DOM mutations happen.
React walks the effect list and applies all DOM mutations synchronously: insertions, updates, deletions. After this, the DOM reflects the new UI.
useLayoutEffect callbacks run synchronously after DOM mutation but before the browser paints. Then the browser paints. Finally, useEffect callbacks run asynchronously after paint.
Pure, side-effect-free phase where React calls components and builds the new fiber tree. Can be interrupted in concurrent mode.
Synchronous phase where React applies DOM mutations and fires lifecycle methods/effects. Cannot be interrupted.
Fires synchronously after DOM mutation, before browser paint. Use for DOM measurements. Blocks painting.
Fires asynchronously after browser paint. Doesn't block painting β ideal for subscriptions, fetches, and non-visual side effects.
In development, React renders components twice in the render phase to detect side effects. Only one commit happens.
1function MyComponent() {2 // Fires: after every render, after browser paint (async)3 useEffect(() => {4 console.log('3. useEffect β DOM painted, async');5 return () => console.log('cleanup');6 });78 // Fires: after every render, before browser paint (sync)9 useLayoutEffect(() => {10 console.log('2. useLayoutEffect β DOM mutated, not painted');11 });1213 console.log('1. Render phase β pure, no side effects');14 return <div />;15}16// Order: Render β DOM mutation β useLayoutEffect β Paint β useEffect
Understanding the pipeline helps you put code in the right place. DOM measurements must go in useLayoutEffect (before paint). Data fetching goes in useEffect (after paint, non-blocking). Initialization that must happen before first render goes in useState initializer or useMemo. Getting this wrong causes visual flashes, layout jumps, or missed renders.
You're building a tooltip that positions itself relative to a target element. Using useEffect, the tooltip first renders at (0,0), then jumps to the correct position β causing a visible flash.
useEffect fires AFTER the browser paints. So the sequence is: Render (tooltip at 0,0) β Paint (user sees misplaced tooltip) β useEffect (measure & reposition) β Re-render β Paint (correct). The first paint shows the wrong position.
Switch to useLayoutEffect which fires AFTER DOM mutation but BEFORE paint. The sequence becomes: Render β DOM mutation β useLayoutEffect (measure & update) β Paint (correct position). The user never sees the wrong position because the browser hasn't painted yet.
Takeaway: Use useLayoutEffect for any DOM measurement or mutation that affects visual layout (tooltips, popovers, scroll position, animations). Use useEffect for everything else (data fetching, analytics, subscriptions).
You add console.log statements to debug when state changes propagate. The logs appear in an unexpected order: render logs appear before effect logs, and cleanup logs appear between them.
Without understanding the pipeline phases, developers assume effects run 'during' rendering. They place side effects in the render function body (causing bugs) or misread the log order and chase phantom timing issues.
The rendering pipeline has strict phases: Render (pure, produces VDOM) β Commit (DOM writes) β Layout Effects (sync, before paint) β Paint (browser) β Effects (async, after paint). Each console.log appears in pipeline order. Understanding this order lets you read debug output correctly and place code in the right phase.
Takeaway: The rendering pipeline is NOT a single step β it's 5 distinct phases with different timing guarantees. Most React bugs come from putting code in the wrong phase (e.g., side effects in render, DOM reads in useEffect instead of useLayoutEffect).