I1 · Part 2 · Intermediate
useRef
A box that survives renders and does not cause them.
08On this page
Two jobs, one hook
useRef returns an object with a single current
property. React keeps that object for the life of the component and never
replaces it. Changing current does not trigger a render.
const ref = useRef(initial); // { current: initial }, same object forever
That one behaviour covers two very different uses.
Job one: reaching a DOM node
function SearchBox() {
const input = useRef(null);
useEffect(() => {
input.current.focus(); // after the DOM exists
}, []);
return <input ref={input} />;
}
React sets input.current to the DOM node after committing, and
back to null on unmount. During the first render it is
null — which is why DOM work goes in an effect, not in the
render body.
Legitimate reasons to hold a DOM node: focus, text selection, scroll position, measuring, playing media, and handing the element to a non-React library like a chart or a map. Reading or writing anything React also controls is not on that list.
Job two: a value that must survive renders
const timer = useRef(null);
function start() {
timer.current = setInterval(tick, 1000); // no re-render, correctly
}
function stop() {
clearInterval(timer.current);
}
A plain variable would be recreated each render; state would re-render the component for a value nothing displays. A ref is exactly the middle case: remembered, but not rendered.
| useState | useRef | |
|---|---|---|
| Survives re-renders | yes | yes |
| Changing it re-renders | yes | no |
| Can be read during render | yes | not safely |
| Updated by | a setter, replacing | assigning to .current |
The rule that keeps refs safe
Do not read or write ref.current during rendering. Rendering
must be pure and repeatable; a ref is mutable state that React does not
track, so reading it mid-render makes the output depend on something React
cannot see. Refs belong in event handlers and effects.
The classic use: previous value
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value; // after render, so reads see the old one
});
return ref.current;
}
const prevCount = usePrevious(count); // undefined on first render
The timing is the whole trick: the effect writes after the render that read it, so the ref is always one render behind.
The other classic: the latest callback
function useInterval(callback, delay) {
const saved = useRef(callback);
useEffect(() => { saved.current = callback; }, [callback]); // always current
useEffect(() => {
const id = setInterval(() => saved.current(), delay);
return () => clearInterval(id);
}, [delay]); // interval not restarted
}
Without the ref, the interval closes over the first render's
callback and calls a stale function forever. Adding
callback to the second effect's dependencies would fix the
staleness by tearing down and recreating the interval on every render, which
makes the timer drift. The ref gives you a fresh function without restarting
anything — this is the pattern behind most "latest ref" code you will
see in libraries.
Callback refs
A ref prop can also be a function. React calls it with the node
when it mounts and with null when it unmounts, which is useful
when you need to run something the moment the node exists.
<input ref={(node) => { if (node) node.focus(); }} />
Define it outside render or memoise it — an inline arrow is a new function each render, so React detaches and reattaches the ref every time.
Callback refs are also the answer to a list of refs, where
useRef cannot help because you do not know the count in advance:
const rows = useRef(new Map());
<li ref={(node) => {
if (node) rows.current.set(item.id, node);
else rows.current.delete(item.id);
}}>
Refs to your own components
In React 19, ref is an ordinary prop for function components, so
a component can accept one and forward it:
function TextField({ ref, ...rest }) {
return <input ref={ref} {...rest} />;
}
Before 19 this required forwardRef, which you will still meet in
every existing codebase and most libraries:
const TextField = forwardRef(function TextField(props, ref) {
return <input ref={ref} {...props} />;
});
useImperativeHandle narrows what the parent gets — instead
of the whole DOM node, expose a small API such as
{ focus, clear }. Reach for it rarely; a component that needs to
be commanded from outside is often one that should have taken a prop.
When a ref is the wrong answer
const count = useRef(0);
count.current++; // ✗ the screen never updates
return <p>{count.current}</p>;
If the value appears on screen, it is state. The question is not "does this change?" but "does the UI need to change when it changes?" — yes means state, no means ref.
Interview answer, one sentence
"A ref is a mutable box that persists across renders without causing them, which makes it right for DOM nodes and for values the UI does not display — timers, previous values, the latest callback. Reading it during render breaks purity, so it belongs in handlers and effects."
Opens in the editor: write it, run it, and check it against real tests.
All 66 for this level →Finished reading?
0 of 57 chapters read in this series