React 102 ⚛️ The Commit Phase vs Render Phase
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
| Aspect | Render Phase | Commit Phase |
|---|---|---|
| Trigger | State update, parent re-render, context change | Render phase completes |
| Interruptible | Yes | No |
| May run multiple times | Yes | No; exactly once |
| DOM mutations | None | All |
| Side effects | Forbidden | Expected |
| Purity requirement | Strict | N/A |
Commit Sub-Phases
| Sub-Phase | What Runs | Timing |
|---|---|---|
| Before mutation | getSnapshotBeforeUpdate | Pre-DOM-change |
| Mutation | DOM changes, cleanup functions | During DOM update |
| Layout | useLayoutEffect, ref attach | After DOM, before paint |
| (Post-paint) | useEffect | After paint |
Effect Types
| Hook | Timing | Blocks Paint |
|---|---|---|
useLayoutEffect | Layout sub-phase | Yes |
useEffect | After paint | No |
useInsertionEffect | Before mutation | CSS-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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Effect runs twice in development | StrictMode double-invoke | Add cleanup function |
| Ref is null in render | Refs attach after commit | Move logic to useLayoutEffect or event handler |
Layout thrash from useLayoutEffect | Synchronous re-render before paint | Use useEffect for non-layout work |
| State update during render | Infinite loop | Move to effect or event handler |
| Intermittent data fetch | Render runs multiple times | Move 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
| Item | Value |
|---|---|
| Render phase | Builds element tree; interruptible; pure |
| Commit phase | Applies DOM changes; atomic; side effects |
| Before mutation | getSnapshotBeforeUpdate |
| Mutation | DOM changes, cleanup functions |
| Layout | useLayoutEffect, ref attach |
| Passive effects | useEffect, after paint |
| StrictMode | Double-invokes render in development |
| Ref timing | Null in render, populated after commit |
| Purity rule | No 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.
useLayoutEffectblocks paint;useEffectdoes 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.currentisnullduring render and populated after the commit. Reading it during render is a mistake that producesnull; 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.
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!