Refs & the DOM
React’s declarative model handles almost everything: you describe the UI as a function of state and React reconciles the DOM for you. But a few tasks are inherently imperative—focusing an input, scrolling an element into view, measuring its size, or controlling a <video>. For those, you need a direct handle on the underlying DOM node, and refs are how React hands one to you without breaking out of the component model.
Getting a DOM node with a ref
Create a ref with useRef(null) and pass it to a JSX element’s ref attribute. After React commits the element to the DOM, it sets ref.current to the real DOM node. Before mount and after unmount, current is null, so guard accordingly.
import { useRef } from "react";
function SearchField() {
const inputRef = useRef(null);
function focusInput() {
inputRef.current?.focus();
}
return (
<div>
<input ref={inputRef} type="text" placeholder="Search…" />
<button onClick={focusInput}>Focus the field</button>
</div>
);
}
The DOM node exposes the full browser API—focus(), select(), scrollIntoView(), getBoundingClientRect(), and so on. React doesn’t wrap or restrict it; you’re talking to the platform directly.
Common imperative jobs
These four cases cover the large majority of legitimate ref usage.
Managing focus. Move keyboard focus after an action—opening a dialog, revealing an inline editor, or recovering focus after a deletion.
Scrolling. Bring an element into view or restore scroll position.
function scrollToTop(ref) {
ref.current?.scrollIntoView({ behavior: "smooth", block: "start" });
}
Measuring layout. Read geometry to position tooltips, build virtualized lists, or trigger animations. Measure inside useLayoutEffect so you read the DOM after it’s committed but before the browser paints, avoiding a visible flicker.
import { useLayoutEffect, useRef, useState } from "react";
function MeasuredBox({ children }) {
const boxRef = useRef(null);
const [height, setHeight] = useState(0);
useLayoutEffect(() => {
if (boxRef.current) {
setHeight(boxRef.current.getBoundingClientRect().height);
}
}, [children]);
return (
<div ref={boxRef}>
{children}
<small>Rendered height: {Math.round(height)}px</small>
</div>
);
}
Media playback. Control elements that hold their own internal state—video, audio, and <canvas>.
import { useRef } from "react";
function VideoPlayer({ src }) {
const videoRef = useRef(null);
const play = () => videoRef.current?.play();
const pause = () => videoRef.current?.pause();
return (
<figure>
<video ref={videoRef} src={src} width={480} />
<div>
<button onClick={play}>Play</button>
<button onClick={pause}>Pause</button>
</div>
</figure>
);
}
The ref lifecycle
React sets ref.current during the commit phase, after mutating the DOM and before running effects. On unmount it sets the ref back to null. This timing is why effects—not render—are the right place to touch a ref: by the time an effect runs, the node is guaranteed to exist.
| Phase | ref.current value |
|---|---|
| During render | null (mount) or previous node (update) |
| After commit, before paint | The DOM node (read in useLayoutEffect) |
| After paint | The DOM node (read in useEffect) |
| After unmount | null |
Never read or write
ref.currentduring rendering. The node may not exist yet, and side effects during render make output unpredictable. Confine ref access to event handlers,useEffect, anduseLayoutEffect.
Ref callbacks
Instead of a ref object, you can pass a function to the ref attribute. React calls it with the DOM node on mount and with null on unmount. Ref callbacks shine when you need to attach refs to a dynamic list of elements, or run setup the moment a node appears.
import { useRef } from "react";
function ItemList({ items }) {
const nodesRef = useRef(new Map());
function scrollToItem(id) {
nodesRef.current.get(id)?.scrollIntoView({ behavior: "smooth" });
}
return (
<ul>
{items.map((item) => (
<li
key={item.id}
ref={(node) => {
const map = nodesRef.current;
map.set(item.id, node);
return () => map.delete(item.id);
}}
>
{item.label}
</li>
))}
</ul>
);
}
In React 19 a ref callback may return a cleanup function, called when the node detaches—mirroring useEffect. In earlier versions React calls the callback with null on detach instead, so do cleanup there. Avoid inline arrow callbacks that recreate every render unless you intend the detach/attach cycle; React re-runs a callback whose identity changes.
Keep imperative code minimal
Refs are an escape hatch, not a default. Reaching for the DOM to set text, toggle classes, or store data that should drive the UI fights React instead of using it. Prefer state and props; drop to a ref only for things React genuinely cannot express declaratively, and keep the imperative surface as small as possible.
// Avoid: mutating the DOM React owns
spanRef.current.textContent = label; // React may overwrite this
// Prefer: let React render it
return <span>{label}</span>;
A safe rule: read from the DOM freely (measure, check focus), but think twice before writing to nodes React renders. Mutating React-managed DOM risks being clobbered on the next render.
Best Practices
- Use refs only for imperative tasks React can’t express: focus, scroll, measurement, media, and third-party DOM libraries.
- Guard every access with
ref.current?.or anullcheck—nodes are absent before mount and after unmount. - Touch refs in event handlers and effects, never during render.
- Measure layout in
useLayoutEffect, notuseEffect, to avoid a flash of wrong geometry. - Use ref callbacks for collections of nodes; return (or handle
nullfor) cleanup when a node detaches. - Don’t write to DOM that React renders—let state and props drive content; React may overwrite manual mutations.
- To expose a child’s imperative API to a parent, combine refs with
useImperativeHandlerather than reaching across components.