skip to content

Refs and DOM Interaction

The escape hatch out of the declarative model: reaching a real DOM node to measure it, focus it, or hand it to a library that knows nothing about React. Interviewers go here to see whether you can integrate imperative code without fighting the render cycle.

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

explore

questions

16

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?

level: middleimportance: must knowfreq 72%

answer

  1. render computes, commit touches the DOM
  2. the node does not exist yet
  3. attached before that commit's effects run
  4. first render sees the useRef initial value
  5. reading it during render is impure too

basics

~20 s

React 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 s

Rendering 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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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

context

open as a page

How would you integrate a third-party library that creates and owns its own DOM — a charting library, a map, a rich-text editor — into a React function component?

level: middleimportance: must knowfreq 60%

basics

~20 s

Render an empty container element with a ref, create the library instance in an effect from that node, keep the instance in a ref, and destroy it in the effect's cleanup. React renders no children inside that container.

open as a page

A React tooltip renders at a default position, then reads the trigger's getBoundingClientRect() inside a useEffect and calls setState to move it. Users see the tooltip flash in the wrong place for one frame. Why does that happen, and what do you change?

level: middleimportance: must knowfreq 68%

basics

~20 s

useEffect runs after the browser has painted, so the unmeasured position is visible for one frame. Move the measure-and-reposition work into useLayoutEffect, which React runs synchronously after it mutates the DOM and before the browser paints.

open as a page

In React, what is a callback ref, and what does React pass to that function when the element mounts and when it unmounts?

level: juniorimportance: should knowfreq 52%

basics

~20 s

A callback ref is a function passed as an element's ref prop instead of a ref object. React calls it with the DOM node when the element attaches, and with null when it detaches, so you can run code at the exact moment a node appears or disappears.

open as a page

A teammate wraps a charting library in a React function component by calling `new Chart(document.getElementById('chart'), config)` directly in the component body, above the returned JSX. What is wrong with initializing it there?

level: juniorimportance: should knowfreq 45%

basics

~20 s

Component bodies must stay pure. That call runs again on every render, targets a node React has not created yet on the first render, and a document lookup collides across instances. Initialize in an effect against a ref instead.

open as a page

A React component passes an inline arrow function as an element's ref prop. Why does React detach and re-attach that ref on every re-render, and how would you stop it?

level: middleimportance: should knowfreq 46%

basics

~20 s

An inline arrow is a new function on every render, and React compares the ref prop by identity. Seeing a different value, it detaches the old callback and attaches the new one. Give the callback a stable identity with useCallback to stop the churn.

open as a page

In React 19, how does a parent component get the DOM node rendered by a child function component, and what changed compared with the forwardRef pattern?

level: middleimportance: should knowfreq 56%

basics

~20 s

In React 19, ref is an ordinary prop on function components. The child declares ref in its props and spreads or passes it to the DOM element it renders; the parent just writes ref={someRef}. forwardRef is no longer needed for this and exists mainly for older code.

open as a page

A React component hands a `<div>` to an editor library that fills it with its own DOM. Why must React never render children inside that div, and what breaks if it does?

level: middleimportance: should knowfreq 40%

basics

~20 s

React owns the children of the elements it renders and diffs them against its own model. Library-inserted nodes are invisible to that model, so a React update can remove or reorder the wrong nodes, or throw when a child it expected is gone.

open as a page

A React sidebar measures its own width once with useLayoutEffect and stores it in state, but the sidebar is user-resizable and the stored width goes stale. How do you keep the measurement current, and what has to happen in the effect's cleanup?

level: middleimportance: should knowfreq 45%

basics

~20 s

Replace the one-shot read with a ResizeObserver created in the effect: observe the node from the ref, set state from each entry's size, and call observer.disconnect() in the cleanup so the observation stops when the component unmounts or the node changes.

open as a page

A React component renders a list of rows and needs programmatic access to each row's DOM node. How do you collect those refs, and what has to happen when a row is removed?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Use one callback ref per row, closed over that row's stable id, writing nodes into a Map held in a single useRef. On detach the callback must delete that entry, otherwise the Map retains removed DOM nodes and later lookups return elements no longer in the document.

open as a page

A React chart wrapper lists its `data` prop in the dependency array of the effect that constructs the chart, so every data update destroys and rebuilds it — the chart flickers and the user's zoom is lost. How would you restructure the component?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Separate lifecycle from synchronisation. One effect with an empty dependency array constructs and destroys the instance, and separate effects react to each prop by calling the library's imperative setters on the instance held in a ref.

open as a page

In development, a React component that creates a map with `L.map(node)` inside an effect ends up with two maps stacked in its container, or the library throws that the container is already initialized. What is React doing, and what is the correct fix?

level: seniorimportance: should knowfreq 50%

basics

~20 s

React StrictMode mounts the component, runs the effect's setup, immediately runs its cleanup, then sets up again, to expose incomplete teardown. Two maps means the cleanup never destroyed the first one, so fix the cleanup rather than the double mount.

open as a page

A React panel renders {open && <div ref={panelRef}>...</div>} and measures panelRef.current inside a useLayoutEffect with an empty dependency array. The height is missing the first time the panel opens and never updates when it is reopened. How would you make the measurement reliable?

level: seniorimportance: should knowfreq 33%

basics

~20 s

An effect with an empty dependency array runs once after the first commit, when the conditional node does not exist yet, and assigning to a ref never schedules anything. Measure from a callback ref instead, which React invokes with the node exactly when it is attached.

open as a page

In a React list of 200 rows, every row runs a useLayoutEffect that reads its own offsetHeight and then writes an inline height style onto the same node. Mounting the list is visibly janky. What is the browser doing, and how would you restructure the measurement?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Each row's style write invalidates layout, and the next row's offsetHeight read forces the browser to recompute it synchronously — 200 forced layouts, all inside the commit before the browser is allowed to paint. Batch the work: read every row first, then apply every write.

open as a page

Your React codebase keeps accumulating components that measure DOM nodes in useLayoutEffect to size or position themselves. As the lead, how do you decide which of those measurements stay in JavaScript and which have to be solved another way?

level: principalimportance: should knowfreq 24%

basics

~20 s

Keep a JavaScript measurement only when the geometry genuinely cannot be expressed declaratively and the answer must be true before paint. Everything CSS can express, everything above the fold, and anything that invalidates constantly should move to CSS or an observer.

open as a page

A shared React component needs to let its parent trigger some behaviour imperatively. How do you decide between forwarding the raw DOM node as a ref, publishing a narrow handle with useImperativeHandle, or keeping it declarative through props?

level: principalimportance: nice to knowfreq 28%

basics

~20 s

Prefer props and state; reach for an imperative surface only for one-shot actions state cannot express, such as focus or scroll. Forward the raw node for thin DOM wrappers, and use useImperativeHandle to publish a small named API when the component owns internal structure.

open as a page