| |

React 102 ⚛️ The Commit Phase vs Render Phase

⏱️ Estimated read: 13 min | |

React’s rendering pipeline is divided into two distinct phases with different rules, different guarantees, and different failure modes. The render phase is where React calls component functions, compares the new element tree to the previous one, and determines what needs to change. The commit phase is where React applies those changes to the DOM, runs layout effects, and updates refs. The distinction is not a naming convention; it is the foundation of React’s correctness model. Code that runs in the render phase must be pure and free of side effects, because React may call it multiple times, discard the result, or run it concurrently. Code that runs in the commit phase is guaranteed to execute exactly once per committed update, synchronously, in a defined order.

Key point: The render phase is speculative and may run more than once. The commit phase is authoritative and runs exactly once. Side effects belong in the commit phase because the render phase has no guarantee that its work will be used.


Why the two phases are separated

The purity requirement. React’s concurrent features depend on the ability to interrupt and restart rendering. If a component function mutates a global variable or writes to the DOM during render, an interrupted render leaves that mutation behind with no way to undo it. The render phase must be pure—it computes output from input and nothing else . React 19’s StrictMode deliberately double-invokes component functions in development to surface impurity, and the same render may be replayed when a concurrent update is interrupted by a higher-priority one.

The consistency requirement. The commit phase is atomic. Once it begins, it applies all DOM mutations, runs all layout effects, and updates all refs without yielding. This means the browser never paints a half-updated UI. If a render is interrupted before the commit phase begins, the DOM is unchanged. The user sees either the old UI or the new UI, never a partial state.

The effect ordering requirement. Effects have ordering rules that depend on a known tree structure. Layout effects run synchronously after DOM mutations and before the browser paints. Passive effects run after the paint. Cleanup functions run before the next effect. These orderings are only meaningful if the commit phase is deterministic and atomic. If effects could run during an interrupted render, the orderings would be undefined.

The ref timing requirement. A ref’s current property is null during render and populated after the commit. Code that reads ref.current during render gets null, which is the correct behavior because the DOM node does not exist yet. Code that reads it in an effect or event handler gets the populated node, which is the correct behavior because the commit has completed. This timing is part of the API contract.


a. The Render Phase: What Happens and What Must Not

The render phase is triggered by a state update, a parent re-render, a context change, or an initial mount. React walks the Fiber tree, calls each component function or class render method, and produces a new element tree. For function components, this means the entire function body executes. Hooks are called in order. The returned JSX becomes the new element tree.

function Profile({ userId }) {
  // RENDER PHASE: this function body runs during render
  // It may run multiple times for a single logical update
  const [name, setName] = useState('');

  // Pure computation is allowed
  const initials = name.split(' ').map(w => w[0]).join('');

  // ❌ Side effect: mutates a global during render
  // document.title = `Profile: ${name}`;

  // ❌ Side effect: network request during render
  // fetch(`/api/users/${userId}`);

  return <div>{initials}</div>;
}

The rules for the render phase are strict. No DOM mutations. No network requests. No subscriptions. No console.log that matters. No reading from mutable global state. No calling Math.random() or Date.now() in a way that affects output, because the render may run again and produce a different result. React’s documentation calls this “keeping components pure” and describes it as a requirement, not a suggestion.

The reason is not stylistic. In concurrent mode, React may start rendering a tree, receive a higher-priority update, discard the tree, and start over. A side effect in the discarded tree has already happened. There is no cleanup for it because the tree was never committed. This is why the render phase must be free of anything that would need to be undone.


b. The Commit Phase: The Three Sub-Phases

The commit phase is triggered when the render phase completes and React has a finished work-in-progress tree. It runs synchronously and cannot be interrupted. It has three sub-phases, each with a defined purpose and ordering.

Before mutation. React reads the DOM state before making changes. This is where getSnapshotBeforeUpdate runs in class components. It is the only point where the pre-mutation DOM is still intact, which is why it exists for use cases like preserving scroll position during a DOM update.

Mutation. React applies all DOM changes. Insertions, updates, deletions, and ref detachments happen here. The useLayoutEffect cleanup functions from the previous render also run here, before the new layout effects. This sub-phase is where the visible DOM changes.

Layout. React runs useLayoutEffect callbacks and attaches refs. These run synchronously after the DOM has been mutated but before the browser paints. Code here can read the new layout and trigger a synchronous re-render if necessary, though that is expensive and should be used sparingly.

function Tooltip({ children }) {
  const ref = useRef(null);
  const [position, setPosition] = useState({ top: 0, left: 0 });

  // LAYOUT sub-phase: DOM exists, browser has not painted
  useLayoutEffect(() => {
    const rect = ref.current.getBoundingClientRect();
    setPosition({ top: rect.bottom, left: rect.left });
  }, []);

  return <div ref={ref}>{children}</div>;
}

After the layout sub-phase, the browser paints. Then passive effects run. useEffect callbacks are scheduled after the paint, which is why they do not block the visual update. The distinction between useLayoutEffect and useEffect is exactly the distinction between the layout sub-phase and the post-paint phase.

function Analytics({ page }) {
  // PASSIVE: runs after paint, does not block visual update
  useEffect(() => {
    trackPageView(page);
  }, [page]);

  return <div>{page}</div>;
}

c. The Ordering Guarantees and the StrictMode Double-Invoke

The commit phase has ordering guarantees that application code depends on. Within a single component, cleanup functions from the previous render run before the new effects. Across the tree, layout effects run child-first (bottom-up), and passive effects also run child-first. Refs are attached before layout effects run, which is why useLayoutEffect can read ref.current and get a populated node.

StrictMode in development deliberately violates the “run once” expectation for the render phase. It double-invokes component functions, state updater functions, and useState/useMemo/useReducer initializers to surface impurity. It also mounts, unmounts, and remounts components to verify that effects clean up correctly. This is not a bug; it is a development-only check that surfaces code that would break under concurrent rendering.

// StrictMode: this runs twice in development
function Component() {
  const [state] = useState(() => {
    // Initializer runs twice in StrictMode
    // Must be pure; no side effects here
    return computeInitialState();
  });

  useEffect(() => {
    // Mount → unmount → mount in StrictMode
    // Cleanup runs between the two mounts
    const sub = subscribe();
    return () => sub.unsubscribe();
  }, []);

  return <div>{state}</div>;
}

The double-invoke is the mechanism that reveals render-phase side effects. A component that writes to the DOM during render will write twice. A component that fetches data during render will fetch twice. A component that mutates a global will mutate twice. In development, this is visible and fixable. In production, the same component may break under concurrent rendering when React discards a render and re-runs it.


Complete Example Session

// ============================================
// PART 1: RENDER PHASE — FUNCTION BODY
// ============================================
function Component({ value }) {
  // This entire body runs during render
  // It may run multiple times for one update
  const doubled = value * 2; // Pure computation: allowed
  return <div>{doubled}</div>;
}


// ============================================
// PART 2: RENDER PHASE — HOOKS
// ============================================
function Component() {
  // Hooks are called during render, in order
  const [count, setCount] = useState(0);
  const memo = useMemo(() => compute(count), [count]);
  return <div>{memo}</div>;
}


// ============================================
// PART 3: RENDER PHASE — SIDE EFFECT VIOLATION
// ============================================
function BadComponent({ name }) {
  // ❌ DOM mutation during render
  document.title = name;

  // ❌ Network request during render
  fetch('/api/track');

  // ❌ Subscription during render
  window.addEventListener('resize', handler);

  return <div>{name}</div>;
}


// ============================================
// PART 4: BEFORE MUTATION SUB-PHASE
// ============================================
class Component extends React.Component {
  getSnapshotBeforeUpdate(prevProps, prevState) {
    // Pre-mutation DOM is still intact
    // Return value passed to componentDidUpdate
    return this.listRef.current.scrollHeight;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    // Use snapshot to restore scroll position
  }
}


// ============================================
// PART 5: MUTATION SUB-PHASE — DOM CHANGES
// ============================================
// React applies:
// - Insertions (new DOM nodes)
// - Updates (changed attributes, text)
// - Deletions (removed DOM nodes)
// - Ref detachments
// The visible DOM changes here.


// ============================================
// PART 6: LAYOUT SUB-PHASE — useLayoutEffect
// ============================================
function Tooltip({ children }) {
  const ref = useRef(null);
  const [pos, setPos] = useState(null);

  useLayoutEffect(() => {
    // DOM exists, browser has not painted
    const rect = ref.current.getBoundingClientRect();
    setPos({ top: rect.bottom, left: rect.left });
  }, []);

  return <div ref={ref} style={pos}>{children}</div>;
}


// ============================================
// PART 7: POST-PAINT — useEffect
// ============================================
function Logger({ page }) {
  useEffect(() => {
    // Runs after paint; does not block visual update
    logPageView(page);
  }, [page]);

  return <div>{page}</div>;
}


// ============================================
// PART 8: CLEANUP ORDER
// ============================================
function Component({ id }) {
  useEffect(() => {
    const sub = subscribe(id);
    return () => sub.unsubscribe();
    // Cleanup for previous id runs before new effect
  }, [id]);

  return <div>{id}</div>;
}


// ============================================
// PART 9: STRICTMODE DOUBLE-INVOKE
// ============================================
// In development, StrictMode:
// - Double-invokes component functions
// - Double-invokes state updater functions
// - Double-invokes useState/useMemo initializers
// - Mounts, unmounts, remounts components
// This surfaces render-phase side effects.


// ============================================
// PART 10: REF TIMING
// ============================================
function Input() {
  const ref = useRef(null);

  // ❌ During render: ref.current is null
  // console.log(ref.current);

  // ✅ During layout effect: ref.current is populated
  useLayoutEffect(() => {
    console.log(ref.current); // <input>
  }, []);

  return <input ref={ref} />;
}

The ten parts trace the render phase, the three commit sub-phases, the effect ordering, and the StrictMode behavior. Part 3 shows the violations that StrictMode catches. Parts 6 and 7 show the distinction between layout effects and passive effects. Part 10 shows the ref timing contract.


Quick Reference

Phase Comparison

AspectRender PhaseCommit Phase
TriggerState update, parent re-render, context changeRender phase completes
InterruptibleYesNo
May run multiple timesYesNo; exactly once
DOM mutationsNoneAll
Side effectsForbiddenExpected
Purity requirementStrictN/A

Commit Sub-Phases

Sub-PhaseWhat RunsTiming
Before mutationgetSnapshotBeforeUpdatePre-DOM-change
MutationDOM changes, cleanup functionsDuring DOM update
LayoutuseLayoutEffect, ref attachAfter DOM, before paint
(Post-paint)useEffectAfter paint

Effect Types

HookTimingBlocks Paint
useLayoutEffectLayout sub-phaseYes
useEffectAfter paintNo
useInsertionEffectBefore mutationCSS-in-JS only

Best Practices

✅ Do This:

// Pure computation in render
const fullName = firstName + ' ' + lastName;                           // ✅
// Side effects in useEffect
useEffect(() => { document.title = title; }, [title]);                  // ✅
// Layout reads in useLayoutEffect
useLayoutEffect(() => { measure(); }, []);                              // ✅
// Ref reads in layout effects or event handlers
useLayoutEffect(() => { ref.current.focus(); }, []);                   // ✅

❌ Don’t Do This:

// DOM mutation during render
function Component() { document.title = 'Title'; return null; }        // ❌
// Network request during render
function Component() { fetch('/api'); return null; }                    // ❌
// Reading ref.current during render
function Component() { console.log(ref.current); return null; }        // ❌
// Subscription during render
function Component() { window.addEventListener('resize', h); }         // ❌

Common Pitfalls

PitfallWhy It HappensFix
Effect runs twice in developmentStrictMode double-invokeAdd cleanup function
Ref is null in renderRefs attach after commitMove logic to useLayoutEffect or event handler
Layout thrash from useLayoutEffectSynchronous re-render before paintUse useEffect for non-layout work
State update during renderInfinite loopMove to effect or event handler
Intermittent data fetchRender runs multiple timesMove fetch to effect

Real-World Examples

1. Pure Render Computation

const sorted = [...items].sort((a, b) => a.name.localeCompare(b.name));

2. DOM Mutation in Effect

useEffect(() => { document.title = title; }, [title]);

3. Layout Measurement

useLayoutEffect(() => { const rect = ref.current.getBoundingClientRect(); }, []);

4. Ref Focus

useLayoutEffect(() => { inputRef.current.focus(); }, []);

5. Cleanup Function

useEffect(() => { const sub = subscribe(); return () => sub.unsubscribe(); }, []);

6. getSnapshotBeforeUpdate

getSnapshotBeforeUpdate() { return this.ref.current.scrollHeight; }

7. Post-Paint Analytics

useEffect(() => { trackPageView(page); }, [page]);

8. StrictMode Double-Invoke

// Component function runs twice in development

9. Ref Timing

// ref.current is null during render, populated in layout effect

10. State Updater Purity

setCount(c => c + 1); // Updater must be pure

Visual

The Render-Commit Pipeline

┌──────────────────────────────────────────────────────────────┐
│  STATE UPDATE                                                │
│         │                                                    │
│         ▼                                                    │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ RENDER PHASE (interruptible)                            ││
│  │                                                         ││
│  │ Call components, run hooks, build element tree          ││
│  │ May be interrupted, discarded, restarted                ││
│  │ Must be pure; no side effects                           ││
│  └──────────────────────────┬──────────────────────────────┘│
│                             │ Tree complete                 │
│                             ▼                                │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ COMMIT PHASE (atomic)                                   ││
│  │                                                         ││
│  │ ┌─────────────────────────────────────────────────────┐ ││
│  │ │ Before mutation: getSnapshotBeforeUpdate           │ ││
│  │ ├─────────────────────────────────────────────────────┤ ││
│  │ │ Mutation: DOM changes, cleanup functions            │ ││
│  │ ├─────────────────────────────────────────────────────┤ ││
│  │ │ Layout: useLayoutEffect, ref attach                 │ ││
│  │ └─────────────────────────────────────────────────────┘ ││
│  └──────────────────────────┬──────────────────────────────┘│
│                             ▼                                │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ BROWSER PAINT                                           ││
│  └──────────────────────────┬──────────────────────────────┘│
│                             ▼                                │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ PASSIVE EFFECTS: useEffect                              ││
│  └─────────────────────────────────────────────────────────┘│
│                                                              │
│  The render phase can yield; the commit phase cannot.        │
│                                                              │
└──────────────────────────────────────────────────────────────┘

StrictMode Double-Invoke

┌──────────────────────────────────────────────────────────────┐
│  DEVELOPMENT (StrictMode)                                    │
│                                                              │
│  ┌─────────────────┐                                        │
│  │ Render phase    │                                        │
│  │ runs twice      │                                        │
│  └────────┬────────┘                                        │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │ Mount effect    │                                        │
│  └────────┬────────┘                                        │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │ Unmount effect  │  Cleanup runs                          │
│  └────────┬────────┘                                        │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │ Mount effect    │  Again                                 │
│  └─────────────────┘                                        │
│                                                              │
│  This surfaces:                                              │
│  - Render-phase side effects (run twice)                     │
│  - Missing cleanup functions (visible as leaks)              │
│                                                              │
│  In production: render runs once, effects mount once.        │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Effect Timing

┌──────────────────────────────────────────────────────────────┐
│  TIME                                                        │
│  ──────────────────────────────────────────────────────────►│
│                                                              │
│  Render ──► Mutation ──► Layout ──► Paint ──► Passive       │
│  (pure)    (DOM)        (sync)      (visual)  (async)        │
│                                                              │
│  ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌───────┐ ┌─────────┐ │
│  │ Render  │ │ DOM     │ │ Layout  │ │ Paint │ │ useEffect│ │
│  │ phase   │ │ changes │ │ effects │ │       │ │         │ │
│  └─────────┘ └─────────┘ └─────────┘ └───────┘ └─────────┘ │
│                                                              │
│  useLayoutEffect: synchronous, blocks paint                  │
│  useEffect: asynchronous, after paint                        │
│                                                              │
│  The difference is whether the effect can afford to wait     │
│  for the browser to paint.                                   │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Ref Timing

┌──────────────────────────────────────────────────────────────┐
│  RENDER PHASE                     COMMIT PHASE               │
│                                                              │
│  ┌─────────────────┐              ┌─────────────────┐       │
│  │ ref.current     │              │ ref.current     │       │
│  │ = null          │              │ = DOM node      │       │
│  └─────────────────┘              └─────────────────┘       │
│                                                              │
│  Reading ref during render:       Reading ref in effect:     │
│  ┌─────────────────┐              ┌─────────────────┐       │
│  │ null            │              │ <input>         │       │
│  │ (node does not  │              │ (node exists)   │       │
│  │  exist yet)     │              │                 │       │
│  └─────────────────┘              └─────────────────┘       │
│                                                              │
│  This is why ref access belongs in effects and event         │
│  handlers, not in the render body.                           │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
Render phaseBuilds element tree; interruptible; pure
Commit phaseApplies DOM changes; atomic; side effects
Before mutationgetSnapshotBeforeUpdate
MutationDOM changes, cleanup functions
LayoutuseLayoutEffect, ref attach
Passive effectsuseEffect, after paint
StrictModeDouble-invokes render in development
Ref timingNull in render, populated after commit
Purity ruleNo side effects in render

Key takeaways:

  • The render phase is speculative; the commit phase is authoritative. React may run the render phase multiple times, discard the result, and run it again. The commit phase runs exactly once for each committed update. Side effects belong in the commit phase because the render phase has no guarantee that its work will be used.
  • The commit phase has three sub-phases with defined ordering. Before mutation reads the pre-change DOM. Mutation applies the changes. Layout runs synchronous effects and attaches refs before the browser paints. Passive effects run after the paint.
  • useLayoutEffect blocks paint; useEffect does not. The choice between them is the choice between synchronous measurement before the visual update and asynchronous work after it. Layout effects are for reading layout and preventing flicker; passive effects are for everything else.
  • StrictMode double-invokes the render phase in development. This is not a bug; it is a check that surfaces render-phase side effects. Code that runs correctly in StrictMode is code that is safe under concurrent rendering.
  • Ref timing is part of the API contract. ref.current is null during render and populated after the commit. Reading it during render is a mistake that produces null; reading it in an effect or event handler is the correct pattern.

Remember: The render phase and the commit phase are not two names for the same thing. They have different guarantees, different rules, and different failure modes. The render phase computes what should be on the screen. The commit phase puts it there. The render phase may be interrupted and restarted. The commit phase cannot. Code that respects this distinction works correctly under concurrent rendering. Code that treats the render phase as if it were the commit phase—writing to the DOM, fetching data, subscribing to events—breaks in ways that are intermittent, difficult to reproduce, and invisible until a concurrent update discards a render that had already produced a side effect.

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!