React path · State and hooks · Topic 7 of 12

Refs and the DOM

Not started

Most of the time React owns the DOM and you only describe it. Some things can't be described: putting focus in an input, scrolling a row into view, measuring an element, handing a node to a charting library. For those you need the real DOM node, and a ref is how you get it. More generally, a ref is a value that persists across renders like state, but that React ignores: changing it doesn't render anything.

Why it matters: Focus, scrolling, measuring and third-party widgets all need the actual DOM node. Refs are the way to reach it, and the way to keep a value across renders without causing one.

  1. 1A ref is a mutable box

    `useRef(initial)` returns the same `{ current }` object on every render. Changing `current` does not trigger a render, and React never looks at it.

  2. 2DOM refs are filled in at commit

    Pass a ref to a host element and React sets `ref.current` to the DOM node during the commit, and back to `null` when the node is removed.

  3. 3Not during render

    Read and write refs in event handlers and effects. During render the DOM may not exist yet, and a value that changes without a render makes the output unpredictable.

1

How it works

The explanation, with examples you can run.

Two uses of one hook

import { useRef } from "react" export function Search() {  const inputRef = useRef<HTMLInputElement>(null) // will hold a DOM node  const searches = useRef(0) // holds a plain value   function handleClick() {    searches.current += 1 // no render needed: the count isn't shown    inputRef.current?.focus()  }   return (    <>      <input ref={inputRef} placeholder="Search" />      <button onClick={handleClick}>Focus</button>    </>  )}

useRef returns the same object on every render of this component instance. React sets inputRef.current to the <input> node during the commit, and back to null if the input is removed. searches is just a box: perfect for a timer id, a previous value, or a count you only send to analytics.

The test for state versus ref: if changing it should change what is on screen, it is state. If not, a ref avoids the render.

Don't read or write refs during render

import { useRef } from "react" export function Bad() {  const renders = useRef(0)  renders.current += 1 // a side effect during render; Strict Mode doubles it  return <p>Rendered {renders.current} times</p> // shows whatever the box held at render time}

Rendering must be pure, and refs are mutable data outside the snapshot. During the first render a DOM ref is still null, and a value written during render can be counted twice or come from a render that was thrown away. Use refs in event handlers and effects, which run after the commit. The one accepted exception is lazy initialisation: if (ref.current === null) ref.current = createExpensiveThing().

Refs to your own components

In React 19, ref is an ordinary prop for function components, so passing it through is just a prop:

import { useRef, type Ref } from "react" function TextField({ label, ref }: { label: string; ref?: Ref<HTMLInputElement> }) {  return (    <label>      {label} <input ref={ref} />    </label>  )} export function Form() {  const nameRef = useRef<HTMLInputElement>(null)  return (    <form onSubmit={(e) => { e.preventDefault(); nameRef.current?.focus() }}>      <TextField label="Name" ref={nameRef} />    </form>  )}

Older code wraps components in forwardRef to do the same; it still works.

Ref callbacks

Instead of a ref object, you can pass a function. React calls it with the node when it is attached. In React 19 the callback can return a cleanup function, which React calls when the node is detached, so setting up and tearing down per node (an observer, a third-party widget) fits in one place:

export function Watched() {  return (    <div      ref={(node) => {        if (!node) return        const observer = new ResizeObserver(() => console.log("resized"))        observer.observe(node)        return () => observer.disconnect()      }}    />  )}

Keep the DOM work small and leave the structure to React: removing or reordering nodes that React rendered confuses the next commit.

2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.You store a click count in `useRef` and display `{countRef.current}`. Clicking increments it. What does the screen show?
  2. 2.When is `inputRef.current` the input element?
  3. 3.In React 19, how does a function component let its parent get a ref to its inner input?

0 of 3 right so far. Get all three to finish this topic.