Command Palette

Search for a command to run...

Hectal
PHASE 8Intermediate ~26 min· topic 2 of 6

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 like focus().
ref attribute
The special JSX attribute ref={myRef} that tells React to put the element's DOM node into myRef.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.

Form.jsxwhole filejsx
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.

Know when the ref is filleddiagram
Rendering diagram…

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.

Measure.jsxwhole filejsx
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.

Gallery.jsxwhole filejsx
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. 1

    Focus on mount

    In the first example, add useEffect(() => { nameRef.current.focus(); }, []); to App. 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, the autoFocus prop does the same.)

  2. 2

    Measure in a normal effect

    In the measuring example, change useLayoutEffect to useEffect. The numbers are the same, but on a slow machine you may see measuring… flash for one frame, because useEffect runs after the paint.

  3. 3

    Scroll smoothly

    In the fruit strip, pass { behavior: "smooth", block: "nearest", inline: "center" } to scrollIntoView. What changes? (The strip glides and centres the item. Respect users who prefer reduced motion: check matchMedia("(prefers-reduced-motion: reduce)") first.)

Code & diagrams

Focus a field and scroll to a section New tab

Neither job has a React prop. The handlers reach the real nodes through refs and call the browser's own methods.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

focused element: <input#name>
scrolled down before: false
scrolled down after: true
When React sets and clears a ref New tab

First 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

Press Run to see the page.

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:         null
Measure a box before the browser paints New tab

The 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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

200 × 40 px
350 × 40 px
Refs to a list: a callback ref and a Map New tab

One 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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

scrolled to Mango - nodes in the Map: 6
scrolled to Apple - nodes in the Map: 5

Break 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.

terminal
$ Console
── what you'll see ──
Uncaught TypeError: Cannot read properties of null (reading 'focus')
An error occurred in the <App> component.
 
Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://react.dev/link/error-boundaries to learn more about error boundaries.

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.

terminal
$ Console
── what you'll see ──
Uncaught NotFoundError: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.
An error occurred in the <li> component.
 
Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://react.dev/link/error-boundaries to learn more about error boundaries.

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 with null or 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 a ResizeObserver in an effect and clean it up, rather than measuring on every render.

  • ▸

    scrollIntoView options: block and inline (start, center, end, nearest) and behavior (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. 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. 2

    The recipe has three steps: create a ref with useRef(null), pass it to a built-in element with the **ref attribute** (<input ref={inputRef} />), and use inputRef.current in an event handler or effect (inputRef.current.focus()). The ref attribute is special: React doesn't put it on the DOM as an HTML attribute; it fills in your ref.

  3. 3

    When React sets and clears refs: during rendering the element doesn't exist yet, so ref.current is null on the first render. React sets ref.current during the commit, right after it inserts the node and before your layout effects and effects run. When the element is removed (unmounted), React sets ref.current back to null. So: read refs in handlers and effects, and check for null when the element might not be on screen (inputRef.current?.focus()).

  4. 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() returns width, height, top, left and more, in CSS pixels.

  5. 5

    A list of elements: you can't call useRef in a loop (hooks must be called in the same order every render, Topic 4.2). Instead, pass a callback ref: a function as the ref attribute. React calls it with the DOM node when the element is attached. Keep one Map in 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. 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 focus or scrollTop) is safe.

Explain it without notes

01

How do you get a DOM node in React, and when is it available?

02

When does React set a ref to null?

03

How do you keep refs to a list of elements whose length changes?

04

What can you safely do with a DOM ref, and what shouldn't you do?

Practice

01

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.

02

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 useLayoutEffect prevents flicker but blocks the paint; measuring in useEffect paints 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() or scrollIntoView().

  • I know a DOM ref is null during the first render and is set before layout effects run.

  • I know React sets the ref back to null when 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.