In React, why is ref.current still null while a component is rendering, and at what point does React actually set it to the DOM node?
answer
- render computes, commit touches the DOM
- the node does not exist yet
- attached before that commit's effects run
- first render sees the useRef initial value
- reading it during render is impure too
basics
~20 sReact attaches DOM refs during the commit phase, after the rendered output is applied to the DOM. While a component renders, the node does not exist yet, so ref.current is still the initial value (null on first render). Read it in effects or event handlers.
solid answer
~50 sRendering in React is a pure calculation: your component function returns a description of the UI, and no DOM node exists yet. React only creates or updates real nodes in the commit phase, and that is when it assigns `ref.current` — before running that commit's layout effects and effects, and after setting the previous ref's `current` back to `null` when a node is removed or the ref changes. So on first render `ref.current` is whatever you passed to `useRef`, normally `null`, and on later renders it holds the node from the *previous* commit, which may not be what ends up on screen if that render is discarded. Reading it during render is therefore both crash-prone and impure. Read it in an event handler, in `useEffect` or `useLayoutEffect`, or in a callback ref — all of which run after attachment.
go deeper
Know that a ref you attach to a DOM element is empty on the first render and filled in afterwards, and that the safe places to use it are event handlers and useEffect.
Be ready to name the two phases and say that React assigns ref.current during commit, before that commit's effects run, and clears it back to null on detach. Explain why the first render necessarily sees the useRef initial value.
Show the purity argument, not just the crash: a render that reads a mutable box filled by a previous commit is impure, and concurrent rendering can discard the render you were reasoning about. Point to StrictMode double rendering as the thing that exposes it.
Frame the render/commit split as the constraint the whole concurrent model buys: because render is replayable and discardable, side channels into the DOM must sit in commit. Be able to say which escape hatches you tolerate in a codebase and how you keep them from spreading.
## Two phases, one rule React does its work in two distinct phases, and refs live entirely in the second one. **Render** is a pure calculation. React calls your component function, and the function returns React elements — plain JavaScript objects describing what the UI should look like. Nothing has touched the DOM at this point. React may call your component more than once, may throw the result away, and (with concurrent rendering) may pause a render and restart it at a different priority. **Commit** is when React takes the description it settled on and applies it: creating, updating, moving and deleting real DOM nodes. Everything that touches the real world — DOM mutations, ref attachment, effects — happens here. The rule follows directly: a ref points at a real node, real nodes only exist after commit, so `ref.current` cannot be meaningful during render. ## What `ref.current` actually holds during render `useRef(initialValue)` returns one stable object of the shape `{ current: initialValue }` that survives across renders. Passing it as `ref` to a host element (`<input ref={inputRef} />`) tells React: after you commit this element, write the DOM node into `.current`. - **First render.** The element has not been created yet, so `inputRef.current` is still exactly what you passed in — `null` in the conventional `useRef(null)`. - **Later renders.** `.current` holds the node from the *previous* commit. That looks useful, but it is a trap: you are reading state produced by a commit that may no longer describe the tree you are currently computing, and if this render is discarded the value never becomes correct for it. ## When React sets and clears it During commit, React writes the node into `.current` for every element carrying the ref. This happens **before** the effects for that commit run, which is the guarantee everything else depends on: by the time your `useLayoutEffect` or `useEffect` callback executes, the refs for the DOM that commit produced are populated. Detachment is the mirror image. When a node is removed, or when you point a different ref at the same element, React sets the old ref's `current` back to `null` so you never hold a pointer to a detached node. ```jsx function SearchBox() { const inputRef = useRef(null); // WRONG: runs during render, current is null on the first pass // inputRef.current.focus(); useEffect(() => { inputRef.current.focus(); // fine: commit already happened }, []); return <input ref={inputRef} />; } ``` ## Why reading it during render is wrong, twice over **It crashes.** On the very first render `inputRef.current` is `null`, so `inputRef.current.focus()` throws a TypeError about reading a property of null. Adding `?.` silences the crash but not the bug — the call simply never happens on the render that mattered. **It breaks purity.** React's contract is that rendering a component with the same props and state produces the same output. A component that reads a mutable box filled in by a previous commit produces output that depends on commit history, so the same inputs can render differently. That is exactly the class of bug that React's development-only double render in StrictMode is designed to surface, and the class that concurrent rendering — where React can render a tree it never commits — turns from theoretical into real. ## Where to read it instead Anywhere that runs after commit: - **Event handlers.** A click handler only fires long after mount, so `inputRef.current` is populated. - **`useEffect` / `useLayoutEffect`.** Both run after refs are attached for that commit. - **Callback refs.** Instead of an object, pass a function as `ref`. React calls it with the node at attach time and (in React 18 and earlier) with `null` at detach time; in React 19 the function may instead return a cleanup function that React runs on detach. A callback ref is the tool when you need to *react* to a node appearing or disappearing rather than just grab it later — which matters for conditionally rendered elements, where an object ref gives you no notification at all. - **Timeouts, subscriptions, promise callbacks** — all scheduled from one of the above. If what you actually need is for something you read from the DOM to influence rendered output, the shape is always the same: render once, read in an effect, call `setState`, render again. There is no way to shortcut that by peeking during render, because during render the answer does not exist yet. ## The nuance worth stating out loud "Don't read `ref.current` during render" is about refs *attached to elements*. A ref you create and write yourself as a mutable box is a different use of the same hook with its own rules, and it is not what an interviewer is probing when they ask why `ref.current` is null.
- If ref.current is populated before effects run, is it safe to read a child's ref inside the parent's useEffect?Yes. React commits children before their parent, and it attaches refs for the whole committed tree before running that commit's effects, so a child's DOM node is already in `.current` by the time the parent's effect fires. The ordering to be careful about is the opposite direction: during render, a parent has no committed child yet.
- What actually happens if you write ref.current.focus() at the top of the component body?On the first render `ref.current` is `null`, so it throws a TypeError immediately and the component never mounts. Even if you guard it with optional chaining, the call is skipped on the render that would have mattered and then runs on every subsequent render, which is both wrong and impure — the fix is to move it into an effect or an event handler.
- Why does React set the old ref's current back to null when a node unmounts?So you cannot keep a live reference to a node that is no longer in the document. Holding a detached node keeps it and its subtree alive in memory and makes imperative calls silently do nothing. The `null` call is also your notification that the node went away, which is why callback refs are useful for attach/detach bookkeeping.
saying these in an interview costs you the question
- Says ref.current is set during render, before the return
- Claims useRef is null because you forgot a dependency array
- Thinks optional chaining during render makes the read correct
- Believes refs update at the same time as state
- Assumes ref.current keeps pointing at a node after unmount