The Render Phase, the Commit Phase, and Fiber
Understand what actually happens when a React component re-renders: the pure render phase, the commit phase that touches the DOM, what triggers a re-render, how parent renders affect children, and how the Fiber architecture makes all of this interruptible.
React's entire rendering model boils down to a strict two-phase pipeline — render, then commit — repeated every time something changes. Understanding where the line between those phases falls explains nearly every subtle React behavior you'll encounter, from why you can't read updated DOM inside a render function to why state updates seem to 'batch' together.
What triggers a render
A component re-renders for exactly two reasons: its own state changed (via a useState setter or useReducer dispatch), or its parent re-rendered, which by default re-renders every child in that subtree regardless of whether that child's own props actually changed. There is no third mechanism — props changing on their own doesn't trigger a render unless it happens as a consequence of the parent re-rendering to produce those new props.
This 'parent re-renders, so do all its children by default' behavior is intentional and simple to reason about, but it does mean that an expensive component nested deep under a frequently-updating parent can re-render far more often than its own data changes would justify — which is exactly the problem React.memo, useMemo, and useCallback (covered elsewhere) exist to address.
It's worth being precise about vocabulary here: a 'render' in React's sense means calling your component function again to produce a new description of the UI — it does not mean the DOM was touched. Whether the DOM actually changes as a result is decided later, in reconciliation and the commit phase.
The render phase: pure and interruptible
During the render phase, React calls your component functions (top-down, starting from the root of the changed subtree) to figure out what the UI should look like. This phase must be pure — no side effects, no mutating external variables, no reading or writing to the DOM — because React may call your component function more than once for a single update, may throw away the result without ever committing it, or may pause and resume the work later. None of this is safe unless your render logic has no observable side effects.
The output of the render phase is not the DOM — it's a tree of React elements (plain JavaScript objects describing type, props, and children). React then compares this new element tree against the previous one during reconciliation to compute the minimal set of actual DOM operations needed.
Because the render phase is just computing a description and not touching the DOM, React is free to interrupt it, throw partial work away, and restart — this is what makes concurrent features possible: a low-priority render (like rendering results for a search box as the user types) can be paused mid-way if a higher-priority update (the next keystroke) comes in, without ever having touched real DOM nodes with incomplete work.
The commit phase: where the DOM actually changes
Once reconciliation has determined the minimal set of changes, React enters the commit phase, where it actually mutates the real DOM: inserting, updating, and removing nodes; attaching event listeners; and running the effects and lifecycle methods that need the DOM to exist (componentDidMount/componentDidUpdate for classes, the useLayoutEffect callback, and afterward useEffect callbacks for functions). Unlike the render phase, the commit phase is synchronous and cannot be paused halfway through — the DOM cannot be left in a half-updated state visible to the user, so once commit starts, React runs it to completion.
This is exactly why side effects (data fetching, subscriptions, manually reading layout) belong in useEffect/useLayoutEffect rather than directly inside the component body: the component body runs during the (interruptible, possibly-repeated) render phase, while effects run during or right after the (guaranteed-to-happen-once-per-committed-render) commit phase.
After commit, the browser gets a chance to paint the updated DOM to the screen. useLayoutEffect callbacks run synchronously after DOM mutations but before the browser paints (useful when you need to measure or adjust layout before the user sees anything), while useEffect callbacks run asynchronously after paint, which is why useEffect is the right default for anything that doesn't need to block the paint, like most data fetching and subscriptions.
Fiber: the architecture behind interruptible rendering
Fiber is the name of React's internal reconciliation engine and the data structure it works with. Instead of a simple recursive call stack (which can't be paused once started), React builds a Fiber tree — a linked-list-like structure of 'fiber' nodes, one per component/element, each holding the component's type, props, state, and pointers to its child, sibling, and return (parent) fiber. Because this tree lives in memory rather than on the JavaScript call stack, React can walk it incrementally, pause between units of work, hand control back to the browser (to stay responsive to input, for example), and resume later.
React actually keeps two fiber trees during an update: the 'current' tree reflecting what's on screen, and a 'work-in-progress' tree being built during the render phase from the new elements. Reconciliation compares the work-in-progress tree against the current tree node by node. When the commit phase finishes, the work-in-progress tree is swapped in as the new current tree (an approach called double buffering) — this is part of why commit must be atomic: swapping only makes sense if it happens all at once.
This architecture is what enables concurrent rendering features: because render-phase work happens on the not-yet-visible work-in-progress tree and can be thrown away or resumed unit-by-unit, React can prioritize urgent updates (like direct user input) over less urgent ones (like a big list re-render) without ever showing the user a torn, half-updated screen.
The full update pipeline
Trigger: initial mount, a state update, or a parent re-rendering. Render: component functions run (pure, interruptible) and produce a new element tree. Reconciliation: the new element tree is diffed against the previous one to compute the minimal DOM changes. Commit: React applies those changes to the real DOM synchronously, then runs layout effects. Browser paint: the updated DOM becomes visible; useEffect callbacks run after this point.
Trigger
↓
Render
↓
Reconciliation
↓
Commit
↓
Browser PaintCode Examples
Clicking the button changes Parent's state, so Parent re-renders. Because Child is rendered from inside Parent, React re-renders Child too by default — even though the label prop passed to it never changes — because 'render' just means calling the function again to produce a new element tree, which is cheap unless the component itself is expensive.
console.log("Child rendered") fires on every click, not just when label changes.
Common mistake: Assuming that because a child's props 'didn't change,' React automatically skips re-rendering it — it doesn't, unless the component is wrapped in React.memo.
useLayoutEffect fires in the same synchronous block as the DOM commit, before the browser has a chance to paint — the right place for layout measurements that must be correct before the user sees anything. useEffect fires later, after paint, which keeps it from blocking the browser and makes it the right default for most side effects. Expected result: Console order: 'layout effect, width: ...' then 'passive effect, after paint'
function Child({ label }: { label: string }) {
console.log("Child rendered");
return <li>{label}</li>;
}
function Parent() {
const [count, setCount] = useState(0);
console.log("Parent rendered");
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>Increment: {count}</button>
{/* Child's own props ("label") never change, yet it re-renders every click */}
<Child label="static text" />
</div>
);
}function MeasureExample() {
const boxRef = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
// Runs synchronously right after commit, BEFORE the browser paints.
console.log("layout effect, width:", boxRef.current?.getBoundingClientRect().width);
});
useEffect(() => {
// Runs asynchronously AFTER the browser has painted.
console.log("passive effect, after paint");
});
return <div ref={boxRef}>Measured box</div>;
}Trigger to paint
Render is pure and interruptible; commit is synchronous and atomic; paint happens after commit, which is why useEffect (post-paint) differs from useLayoutEffect (pre-paint, post-commit).
Trigger
↓
Render
↓
Reconciliation
↓
Commit
↓
Browser PaintImportant Rules
A component re-renders only because its own state changed or because its parent re-rendered — there is no other trigger.
The render phase must be free of side effects, because React may call it more than once or discard its result before committing.
The commit phase is synchronous and atomic — the DOM is never left in a partially-updated state that the user can see.
useLayoutEffect runs synchronously after commit but before paint; useEffect runs asynchronously after paint.
Fiber's in-memory tree (rather than the plain call stack) is what makes render-phase work pausable, resumable, and priority-ordered.
Where this shows up
A typeahead search box that re-renders its whole results list on every keystroke, while a slow-updating background chart sits elsewhere on the page, relies on Fiber's ability to prioritize the urgent keystroke-driven render over the less urgent background one — this is exactly the scenario concurrent rendering (built on the Fiber architecture) is designed to keep responsive.
function Profile({ userId }: { userId: string }) { document.title = Profile ${userId}; // side effect directly in render return <div>...</div>; } function Profile({ userId }: { userId: string }) { useEffect(() => { document.title = Profile ${userId}; }, [userId]); return <div>...</div>; }
Mutating something outside the component (like document.title) directly during render violates render-phase purity — React may call this function multiple times or discard the result, so the side effect belongs in useEffect, which only runs after a render has actually committed.
Which of the following, by itself, triggers a component to re-render? A. The component's parent re-rendering B. A prop object elsewhere in memory being mutated without any state update C. The browser resizing the window with no state change involved D. Another unrelated component on the page updating its own state
A. The component's parent re-rendering — A component re-renders because its own state changed or because its parent re-rendered (which by default re-renders all its children too). Mutating a prop object in place, without going through a state setter, does not trigger a re-render at all.
Why must the render phase be free of side effects? A. Because side effects are slower than pure functions B. Because React may call a component function more than once, or discard its output, before ever committing it to the DOM C. Because TypeScript forbids side effects inside functions D. Because the commit phase runs before the render phase
B. Because React may call a component function more than once, or discard its output, before ever committing it to the DOM — Render-phase work is speculative from React's point of view — it can be paused, thrown away, or repeated — so anything with an observable side effect (network calls, DOM mutation, mutating external variables) has to be deferred to the commit phase or to effects, which are guaranteed to run only after a render actually commits.
What role does the Fiber tree play in React's rendering model? A. It stores your CSS styles for faster lookup B. It replaces the plain JavaScript call stack with an in-memory, linked structure that React can pause, resume, and prioritize while performing render-phase work C. It is only used for server-side rendering, not in the browser D. It directly represents the real DOM nodes on screen
B. It replaces the plain JavaScript call stack with an in-memory, linked structure that React can pause, resume, and prioritize while performing render-phase work — Fiber is React's internal data structure and reconciliation engine: because the tree of work lives in memory rather than solely on the call stack, React can interrupt rendering, resume it later, and prioritize urgent updates — the foundation for concurrent rendering.
A teammate says: 'I moved my API call and a DOM measurement into the component body instead of useEffect, so it runs earlier.' Explain, in terms of the render/commit pipeline, why this is risky.
Code in the component body runs during the render phase, which is supposed to be pure and can be called more than once by React (or have its output discarded entirely) before anything commits — so an API call there could fire multiple times for what looks like one update, and a DOM measurement would likely fail or read stale values because the real DOM for this render hasn't been created or updated yet (that only happens in commit). Effects run after commit specifically so the DOM is guaranteed to exist and so the side effect runs once per committed render, not once per render-phase call.
Think about what guarantees exist about the render phase potentially running more than once, and about DOM nodes not existing yet.