Render & Commit Phases
Every React update happens in two distinct phases: a render phase where React figures out what the UI should be, and a commit phase where it actually applies those changes to the DOM. The two have completely different rules — one is pure and discardable, the other is the only place real side effects belong. Knowing which phase your code runs in is the single most reliable way to avoid bugs around effects, refs, and concurrent rendering. This page maps the timeline of an update and shows what is safe (and unsafe) to do in each phase.
The render phase
The render phase is React calling your component functions to build a new tree of elements, then diffing that tree against the previous one (reconciliation). It is pure and produces no visible change to the screen. Because React 18+ renders concurrently, this phase can be paused, resumed, restarted, or thrown away entirely before it ever reaches the DOM.
That guarantee only holds if your render logic is pure. During render you may compute values from props and state, but you must not mutate external variables, start fetches, set timers, write to the DOM, or call state setters of other components. If React restarts a render and your code has already mutated something outside, you get inconsistent state and tearing.
function Invoice({ items, taxRate }) {
// Pure: derived entirely from props, safe to run any number of times.
const subtotal = items.reduce((sum, item) => sum + item.price, 0);
const total = subtotal * (1 + taxRate);
return (
<p>
Total: {new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(total)}
</p>
);
}
Warning: Anything you do during render must be safe to repeat. React’s Strict Mode deliberately double-invokes component bodies in development to surface impure render logic — if your component breaks under double rendering, it has a side effect in the wrong phase.
The commit phase
Once React has a finished, reconciled tree, it enters the commit phase. This phase is synchronous and cannot be interrupted — React applies the changes in one uninterruptible pass so the user never sees a half-updated screen. This is the only phase that touches the host environment: it mutates the DOM, attaches and detaches event handlers, and sets ref values (ref.current is null during render and populated during commit).
Effects run as part of (or just after) commit, in a fixed order:
| Step | When it runs | Use for |
|---|---|---|
| DOM mutation | Synchronously, during commit | React inserts/updates/removes nodes |
| Ref assignment | Immediately after mutation | ref.current becomes the real node |
useLayoutEffect | Synchronously, before paint | Measure layout, sync-set DOM to avoid flicker |
| Browser paint | After layout effects finish | Pixels appear on screen |
useEffect | Asynchronously, after paint | Fetches, subscriptions, logging, timers |
The distinction between the two effect hooks is purely about timing. useLayoutEffect blocks paint, so it can read the freshly committed DOM (e.g. measure an element’s width) and write back before the user sees anything — at the cost of delaying the frame. useEffect runs after the browser has painted, so it never blocks rendering and is the right default for almost everything.
import { useState, useRef, useLayoutEffect, useEffect } from "react";
function Tooltip({ label }) {
const ref = useRef(null);
const [width, setWidth] = useState(0);
// Runs before paint: read committed DOM, adjust without flicker.
useLayoutEffect(() => {
setWidth(ref.current.getBoundingClientRect().width);
}, [label]);
// Runs after paint: side effect that doesn't block the frame.
useEffect(() => {
console.log(`Tooltip painted, measured width: ${width}px`);
}, [width]);
return <span ref={ref}>{label}</span>;
}
Output:
Tooltip painted, measured width: 84px
Timeline of one update
state / props / context change
│
▼
┌──────────────── RENDER PHASE (interruptible, pure) ────────────────┐
│ call components → build element tree → reconcile / diff │
│ refs are null · no DOM · no side effects · may restart or discard │
└──────────────────────────────┬─────────────────────────────────────┘
▼ (committed tree is final)
┌──────────────── COMMIT PHASE (synchronous, atomic) ────────────────┐
│ mutate DOM → set refs → run useLayoutEffect → browser paint │
└──────────────────────────────┬─────────────────────────────────────┘
▼
run useEffect (passive, after paint)
The cleanup functions of effects mirror this order: layout-effect cleanups run synchronously before re-running layout effects, and passive-effect cleanups run before re-running passive effects on the next commit (and on unmount).
Why the split matters
Because the render phase is discardable, React can start rendering a low-priority update, abandon it when a more urgent one (like a keystroke) arrives, and restart later. Any side effect performed during render would leak from those abandoned attempts. Pushing side effects into the commit-phase effect hooks is what makes concurrent features like startTransition and Suspense safe. The commit phase, by contrast, is kept atomic precisely so concurrency never produces a torn UI.
Best practices
- Keep render pure: no fetches, subscriptions, timers, DOM writes, or external mutations in the component body.
- Read and set refs in effects or event handlers, never during render —
ref.currentisnulluntil commit. - Default to
useEffect; reach foruseLayoutEffectonly when you must measure or mutate the DOM before paint to prevent visible flicker. - Don’t depend on
useLayoutEffectfor data fetching — it blocks paint and hurts perceived performance. - Verify components survive Strict Mode’s double render and double-fire effects; if they don’t, a side effect is in the wrong phase.
- Treat commit as atomic: don’t assume you can observe intermediate DOM states between renders.