Topic 8.2
Reaching the DOM with Refs
In one line
Put a ref on a JSX element with the ref attribute and React stores the real DOM node in ref.current after the commit, so you can focus it, scroll it into view or measure it; for a whole list of elements, use a callback ref that fills a Map.
Think of it like this
A coat check at a theatre. You hand over your coat and get a numbered ticket. Later you show the ticket and get your exact coat back. Writing ref={inputRef} on an element is asking React for a ticket: after React builds the real element, it puts it in inputRef.current, and you can get hold of that exact element whenever you need it.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- DOM node
- The real browser object for an element, such as an
HTMLInputElement. It has methods likefocus(). - ref attribute
- The special JSX attribute
ref={myRef}that tells React to put the element's DOM node intomyRef.current. - Focus
- Which element receives the keyboard.
input.focus()puts the cursor in a text box. - scrollIntoView
- A DOM method that scrolls the page or a box until the element is visible.
- getBoundingClientRect
- A DOM method that returns an element's size and position on screen, in pixels.
- Callback ref
- A function passed as
ref. React calls it with the node when the element is attached. - Map
- A JavaScript collection of key → value pairs, here item id → DOM node.
- Escape hatch
- A way to step outside React's normal model for the few jobs it can't do itself.
Step by step
01Attach a ref to an element
Create the ref with null (there is no element yet) and pass it to the element with ref.
const nameRef = useRef(null);
return (
<>
<input ref={nameRef} placeholder="Your name" />
<button onClick={() => nameRef.current.focus()}>Edit name</button>
</>
);02Use the node in a handler
When the button is clicked, the input exists, so nameRef.current is the real <input> and focus() puts the cursor in it. This is the classic "jump to the field" pattern, also used to focus the first invalid field in a form (Topic 18.3).
03Know when the ref is filled
During the first render the ref is still null. React fills it in the commit, then runs layout effects, then the browser paints, then effects run. When the element unmounts, React sets the ref back to null before the effects of that update run.
04Measure after the commit
In useLayoutEffect the node is in the DOM and has a size, but the screen isn't painted yet. Measure there when the size decides what you draw.
const boxRef = useRef(null);
const [width, setWidth] = useState(0);
useLayoutEffect(() => {
setWidth(boxRef.current.getBoundingClientRect().width);
}, []);05Many elements: a callback ref and a Map
For a list, give each item a ref callback that stores its node in a Map keyed by the item's id. The cleanup removes it again. Later, map.get(id) finds any item's node.
const nodesRef = useRef(new Map());
{items.map((item) => (
<li
key={item.id}
ref={(node) => {
nodesRef.current.set(item.id, node);
return () => nodesRef.current.delete(item.id); // React 19 cleanup
}}
>
{item.name}
</li>
))}06Change only what React doesn't manage
Focus, scroll position, selection, media playback and measuring are safe. Adding or removing children, or changing text, inside an element React renders is not: React's next update works from its own record of the DOM and can undo your change or throw an error.
Try it yourself
- 1
Focus on mount
In the first example, add
useEffect(() => { nameRef.current.focus(); }, []);toApp. Predict: is the input focused as soon as the page loads? (Yes. The effect runs after the commit, when the ref is set. For an input, theautoFocusprop does the same.) - 2
Measure in a normal effect
In the measuring example, change
useLayoutEffecttouseEffect. The numbers are the same, but on a slow machine you may seemeasuring…flash for one frame, becauseuseEffectruns after the paint. - 3
Scroll smoothly
In the fruit strip, pass
{ behavior: "smooth", block: "nearest", inline: "center" }toscrollIntoView. What changes? (The strip glides and centres the item. Respect users who prefer reduced motion: checkmatchMedia("(prefers-reduced-motion: reduce)")first.)
Code & diagrams
Neither job has a React prop. The handlers reach the real nodes through refs and call the browser's own methods.
Preview
Console
Nothing logged yet.Expected console output
focused element: <input#name>
scrolled down before: false
scrolled down after: trueFirst render: `null`, because the node doesn't exist yet; both effects see the node. When the box is hidden, the render still sees the old node (rendering happens before the commit), and the effects see `null` because React cleared the ref during the commit.
Preview
Console
Nothing logged yet.Expected console output
render: null
layout effect: <div.box>
effect: <div.box>
--- hide the box ---
render: <div.box>
layout effect: null
effect: nullThe box takes whatever space is left in its row after the 40 px icon and the 10 px gap, so its width is decided by layout, not by your code. The layout effect reads it after the commit and before the paint, so the screen never shows `measuring…`.
Preview
Console
Nothing logged yet.Expected console output
200 × 40 px
350 × 40 pxOne ref holds a Map of every item's node. The callback ref adds a node when its `<li>` is attached, and its cleanup removes it when the `<li>` is removed, so after Papaya goes the Map has five nodes.
Preview
Console
Nothing logged yet.Expected console output
scrolled to Mango - nodes in the Map: 6
scrolled to Apple - nodes in the Map: 5Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Use a ref during the first render
In the first example, add nameRef.current.focus(); directly in the body of App, above the return.
Break #2
Remove React's children by hand
In the fruit example, add a button whose handler runs document.querySelector(".strip").innerHTML = "", click it, then click Remove last.
Myth vs fact
Myth
ref.current is available as soon as the component runs.
Fact
On the first render it is null. React sets it during the commit, before layout effects run. Read it in effects and handlers.
Myth
You need one useRef per list item.
Fact
Hooks can't be called in a loop. One ref holding a Map, filled by callback refs, handles any number of items.
Myth
Using refs to the DOM means you're doing React wrong.
Fact
Focus, scrolling, measuring and media control can only be done through the DOM. Refs are the official way. The mistake is using them to change content React renders.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
React attaches refs in the commit's layout phase, in the same pass that runs
useLayoutEffect, child before parent. So a parent's layout effect can already read refs to its children's nodes. On an update, React detaches (calls withnullor runs the cleanup) before attaching the new value. - ▸
Measuring forces the browser to calculate layout synchronously (a forced reflow). Reading
getBoundingClientRect()after DOM writes in a loop causes layout thrashing. For sizes that change over time, use aResizeObserverin an effect and clean it up, rather than measuring on every render. - ▸
scrollIntoViewoptions:blockandinline(start,center,end,nearest) andbehavior(auto,smooth).block: "nearest"avoids scrolling the whole page when only an inner box needs to move. - ▸
Focus management is an accessibility job: when a dialog opens, move focus into it; when it closes, return focus to the button that opened it (Topic 8.5 and Phase 18). Refs are how you do both.
Remember this
- 1
React normally hides the DOM node (the real browser object behind an element, like an
HTMLInputElement) from you, because you describe the screen and React does the DOM work. But some things can only be done by calling a method on the node:focus(),scrollIntoView(),getBoundingClientRect()to measure,play()on a video,select()to highlight text. React has no props for these. A ref is how you reach the node. - 2
The recipe has three steps: create a ref with
useRef(null), pass it to a built-in element with the **refattribute** (<input ref={inputRef} />), and useinputRef.currentin an event handler or effect (inputRef.current.focus()). Therefattribute is special: React doesn't put it on the DOM as an HTML attribute; it fills in your ref. - 3
When React sets and clears refs: during rendering the element doesn't exist yet, so
ref.currentisnullon the first render. React setsref.currentduring the commit, right after it inserts the node and before your layout effects and effects run. When the element is removed (unmounted), React setsref.currentback tonull. So: read refs in handlers and effects, and check fornullwhen the element might not be on screen (inputRef.current?.focus()). - 4
Measuring (reading the size or position of an element) must happen after the element is in the DOM. Use
useLayoutEffect(Topic 7.8) when the measurement changes what you draw, such as placing a tooltip, so the user never sees a wrong frame; use an event handler when you measure in response to a click.el.getBoundingClientRect()returnswidth,height,top,leftand more, in CSS pixels. - 5
A list of elements: you can't call
useRefin a loop (hooks must be called in the same order every render, Topic 4.2). Instead, pass a callback ref: a function as therefattribute. React calls it with the DOM node when the element is attached. Keep oneMapin a ref, add each node under its item's id in the callback, and remove it when the element goes away. In React 19 the callback can return a cleanup function, which React calls when the element is removed (Topic 8.3 looks at this in detail). - 6
Refs to the DOM are an escape hatch: use them for things React can't express (focus, scroll, measure, media, third-party widgets). Don't change what React manages through a ref: don't remove React's children, set their text or add elements inside them by hand. React doesn't know about your changes, so its next update can undo them or crash. Changing things React doesn't manage (like
focusorscrollTop) is safe.
Explain it without notes
How do you get a DOM node in React, and when is it available?
When does React set a ref to null?
How do you keep refs to a list of elements whose length changes?
What can you safely do with a DOM ref, and what shouldn't you do?
Practice
Build a chat box: a list in a scrolling container and an input. After each new message is added, scroll the container to the bottom.
Make a CopyField with a read-only input and a Copy button that selects all the text in the input.
Trade-offs
- ↔
DOM refs give you full browser power but bypass React's model: what you do through them isn't in state, so it isn't restored on re-mount and isn't visible in DevTools.
- ↔
Measuring in
useLayoutEffectprevents flicker but blocks the paint; measuring inuseEffectpaints sooner but may show one wrong frame. Pick by whether the measurement changes what the user sees.
Done when you can
I can attach a ref to an element and call
focus()orscrollIntoView().I know a DOM ref is
nullduring the first render and is set before layout effects run.I know React sets the ref back to
nullwhen the element is removed.I can measure an element in
useLayoutEffect.I can keep refs to a list of elements with a callback ref and a
Map.I know not to change DOM content that React manages.