Lesson 45 of 80
useRef gives you a stable box to read later. A callback ref gives you a function React calls the instant the DOM node shows up — no effect required.
13 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
useRef is the ref API most people learn first: it hands back a stable { current: null } object, React fills in current once the DOM node mounts, and you read .current — usually inside a useEffect — whenever you need it. That's the object-ref style.
A callback ref skips the object entirely. Instead of ref={myRef}, you pass ref={(node) => { ... }} — a function. React calls that function with the DOM node the instant it's attached, and calls it again with null the instant it's detached. There's no intermediate object to read .current off of later; the node arrives as an argument, exactly when it exists.
function AutoSizingPanel() {
const [height, setHeight] = useState(0);
const measureRef = useCallback((node) => {
if (node) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return (
<div ref={measureRef} className="panel">
Panel is {height}px tall
</div>
);
}
With useRef, measuring on mount means: attach the ref, then reach for a useEffect with an empty dependency array to read .current after the render commits — because during render, .current is still null. The callback ref collapses that two-step dance into one: React invokes it the moment the node is attached, so node is guaranteed non-null and already in the DOM, no effect needed at all. This matters most when the element you're measuring can mount and unmount repeatedly (a conditionally-rendered node, an item re-entering a list) — a callback ref reacts to that specific node's attach/detach, where an effect keyed on a ref object wouldn't re-run just because the underlying DOM node changed.
React 19 adds a real quality-of-life feature here: a callback ref can now return a cleanup function, exactly like useEffect does.
const measureRef = useCallback((node) => {
if (!node) return;
const observer = new ResizeObserver(() => {
setHeight(node.getBoundingClientRect().height);
});
observer.observe(node);
return () => observer.disconnect(); // React 19: called automatically on detach
}, []);
Before React 19, you had to manually handle the null case yourself (if (node === null) { cleanup(); return; } inside the same callback) to tear down anything you set up on attach. Now React calls the returned function for you when the node unmounts, the same lifecycle shape effects already have.
null, once with the new node — because React sees a new function reference each time and treats it as "the old ref detached, a new one attached." Wrapping it in useCallback (or defining it outside the component) is usually what you actually want.useRef's .current doesn't trigger a re-render when it changes, and neither does a callback ref firing — in the example above, setHeight is what causes the re-render, not the ref itself. Refs of both kinds are fundamentally about escaping React's render cycle, not participating in it.Both ref styles exist to answer the same need: sometimes you need an actual, imperative handle on something React normally manages for you — this is the same motivation behind forwardRef and useImperativeHandle in this track's ref-exposure lesson, just applied to a DOM node directly instead of a custom component's internals.