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.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.