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?
answer
- render decides, commit applies
- no DOM exists while the body runs
- refs are wired up while committing
- effects come after attachment
- removal sets it back to null
basics
~20 sRendering 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 sRunning 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 linesimport { 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
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.
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.
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.
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