skip to content

The Commit Phase and Effect Order

Once React has decided what the tree should look like, the commit phase applies it to the DOM in one synchronous pass — mutating nodes, attaching refs, then running effects. Interviewers use the render/commit split to test whether you know why useLayoutEffect blocks paint and useEffect does not.

part ofReactoverview, primer and where to startread it →
on this pageshow

explore

questions

4

In a React function component that renders <input ref={inputRef} />, reading inputRef.current in the component body gives null, but reading it inside an effect gives the DOM element. What happens in between?

level: juniorimportance: must knowfreq 60%

answer

  1. render decides, commit applies
  2. no DOM exists while the body runs
  3. refs are wired up while committing
  4. effects come after attachment
  5. removal sets it back to null

basics

~20 s

Rendering only computes what the UI should be, so no DOM node exists yet and the ref is still null. React then commits that output: it creates the DOM nodes, attaches refs to them, and only afterwards runs your effects.

solid answer

~40 s

Running the component body is the render phase. It returns a description of the UI; React has not touched the DOM yet, so there is no element for the ref to point at and `inputRef.current` is still the initial value you passed to `useRef` — `null`. React then enters the commit phase, where it actually creates or updates the DOM nodes and, as part of that same pass, sets `inputRef.current` to the node. Effects run after that, which is why the ref is populated in `useEffect` or `useLayoutEffect`. The same mechanism runs in reverse on the way out: when a node is removed or replaced, React sets the ref back to `null` while it is mutating the DOM, so a ref never points at an element that has already left the document.

code

jsx · 13 lines
jsx
import { useRef, useEffect } from 'react';

export default function FocusInput() {
  const inputRef = useRef(null);
  console.log('during render:', inputRef.current); // null on first render

  useEffect(() => {
    console.log('after commit:', inputRef.current); // the <input> element
    inputRef.current.focus();
  }, []);

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

go deeper

for a junior

Be able to say that the component body runs before React creates any DOM, so a ref is null there, and that effects are the place to touch the node.

for a middle

Explain the render/commit split as the reason: render must stay pure and repeatable, so React attaches refs during the commit, after the nodes exist and before effects run.

for a senior

Show the detachment side too — React nulls refs while removing nodes — and treat a captured ref.current held across an await or a timer as a bug waiting to happen.

for a principal

Frame it as an API boundary: imperative escape hatches are only safe at defined points in the commit, so review guidelines should push teams toward declarative rendering and confine ref use to genuine integrations.

## The two moments Every update in React has a moment where it *decides* and a moment where it *applies*. **Deciding** is the render phase: React calls your component function. The function returns elements — plain objects describing what the UI should look like. Nothing has been created in the document at this point. Even on a re-render, React has not yet applied any change. Because there is no node, there is nothing to assign to a ref, so `inputRef.current` holds whatever you initialised it with. **Applying** is the commit phase. React walks the work it decided on and mutates the real DOM: creating elements, setting attributes, inserting and removing nodes. Ref attachment is part of that pass — once a node exists, React assigns it to the ref before it hands control back. Effects come after. Both `useLayoutEffect` and `useEffect` run once the commit has attached refs, which is the whole reason those hooks are where DOM work belongs. ```jsx const inputRef = useRef(null); console.log(inputRef.current); // null on the first render useEffect(() => { console.log(inputRef.current); // <input> }, []); return <input ref={inputRef} />; ``` ## Why React cannot just fill it in earlier The render phase is required to be pure: given the same props and state it should produce the same output and touch nothing outside itself. React relies on that so it can call a component more than once, discard the result of a render it no longer needs, or re-run work at a different priority. A DOM node is the definition of outside state. If refs were populated during render, every component would be tempted to read and write the document from a function React reserves the right to run speculatively — and the ref would have to point at *something*, even in a render whose output is never committed. ## Detachment matters too Attachment gets all the attention, but the reverse direction is what keeps refs safe. During the commit's mutation work, before or as a node is removed or replaced, React sets its ref back to `null`. That ordering is deliberate: it guarantees that at no point can you read `ref.current` and receive an element that has already been taken out of the document. When a node is replaced rather than removed, the same commit detaches the old ref and attaches the new one, so within a single update a ref never briefly points at the wrong element. This is why code that stashes `ref.current` in a variable and uses it later — inside a timer, a promise continuation, a debounced handler — is risky. The variable keeps a strong reference to a node that React has already detached and the browser may have discarded from the document. Reading `ref.current` at the moment you need it, and checking for `null`, is the habit to demonstrate. ## Second render, first render On the first render the ref is `null` because nothing exists yet. On subsequent renders the component body runs again *before* the new commit, so `ref.current` still holds the node from the previous commit. That is often harmless, but it is not "the current DOM": if this render is going to replace or remove the element, the value you read during the body is about to be stale. Treat the body as a place that has no valid view of the DOM at all, regardless of what it happens to contain. ## Where this shows up in real code The three classic cases are all commit-timing cases. Focusing an input on mount works from an effect and silently does nothing if attempted during render. Handing a node to an imperative library must happen after attachment, and the teardown must happen before detachment — which is exactly what an effect and its cleanup give you. And reading a size or position requires a node that is actually in the document, so it can only happen after the commit has put it there. ## The interview answer Keep it to the two phases: "the component body runs in the render phase, before React has created any DOM, so the ref is still `null`; React attaches refs while committing the result to the document, and effects run after that, so the ref is populated by the time your effect body executes." If you want to show a bit more, add the mirror image — React sets refs back to `null` while removing nodes, which is why a ref never points at a detached element.

  • On the second render of that component, is inputRef.current still null when the body runs?
    No — it holds the node attached by the previous commit, because the body runs before the new commit. That value is not a reliable view of the current DOM, though: if this update removes or replaces the element, what you read is about to be stale. Treat the body as having no valid DOM access at all.
  • What does React do to a ref when the element it points at is removed?
    It sets it back to `null` while it is mutating the DOM, before the removal is complete. That guarantee is why a ref never hands you a node that has already left the document — and why stashing `ref.current` in a variable for use inside a later timer or promise can leave you holding a detached element.
  • If reading a ref during render is unsafe, where does that leave code that needs the node's size?
    In an effect, where the node is attached. Measuring requires a committed DOM, so the read has to happen after the commit; whether it belongs in a layout effect or a passive one depends only on whether the user would otherwise see an uncorrected frame.

saying these in an interview costs you the question

  • Says the DOM exists as soon as the component returns JSX
  • Reads ref.current during render and expects the node
  • Thinks useRef itself queries the DOM
  • Believes a ref keeps pointing at a removed element
  • Confuses the ref object with its current property

context

open as a page

React splits an update into a render phase and a commit phase. Walk through what React does during the commit phase, in order, and say where the browser's paint fits into that sequence.

level: middleimportance: must knowfreq 55%

basics

~20 s

React commits in three sub-phases: it takes pre-mutation snapshots, then mutates the DOM and detaches old refs, then attaches new refs and runs layout effects. The browser paints after that, and useEffect callbacks flush later.

open as a page

In a React tree, a parent component and its child both register callbacks with useEffect. On mount, whose callback runs first, and when a single commit changes effects in several components, how does React order the cleanups against the new callbacks?

level: middleimportance: should knowfreq 42%

basics

~20 s

The child's callback runs first: React runs effects bottom-up, deepest component first, so a parent's effect sees fully mounted children. Within one commit React runs every affected component's cleanup before running any of the new callbacks.

open as a page

A React component positions a tooltip by writing to the DOM node inside a useEffect callback, and users see the tooltip flash at the wrong position for one frame. Explain the timing that causes the flash, and what changes if the identical code runs in useLayoutEffect instead.

level: seniorimportance: should knowfreq 50%

basics

~20 s

React commits the DOM, the browser paints the un-positioned tooltip, and only then does the useEffect callback move it, so one bad frame is visible. A layout effect runs inside the commit before paint, so the user only ever sees the corrected position.

open as a page