A box that survives renders
The rule follows from the table: if the screen depends on it, it is state. If it does not, it is a ref.
const timer = React.useRef(null); // { current: null }
timer.current = setInterval(tick, 1000); // changing it renders NOTHING
useState | useRef | |
|---|---|---|
| Survives a render | yes | yes |
| Changing it re-renders | yes | no |
| Read during render | yes | avoid — it may be stale on screen |
| Change during render | never | never |
| For | anything the user sees | anything the user does not |
Reaching a DOM node
ref.currentisnullon the first render — the element does not exist yet. It is filled in before effects run.- React sets it to the node on mount and back to
nullon unmount. - Never read
ref.currentduring render: at that moment it belongs to the previous commit.
function Search() {
const input = React.useRef(null);
return (
<>
<input ref={input} />
<button onClick={() => input.current.focus()}>Focus</button>
</>
);
}
What refs are legitimately for
- Focus, selection, and scrolling into view.
- Measuring:
getBoundingClientRect,scrollHeight— geometry React does not know. - Imperative media:
video.play(),canvas.getContext("2d"), a map or chart library that owns its own DOM. - Values between renders: an interval id, a previous value, whether the first render has happened, a WebSocket.
Setting node.textContent or node.style.display behind React’s back works until the next render throws it away. Anything visible belongs in state.
The previous value
Writing to the ref inside an effect is the important part: the effect runs after the render, so during the render the ref still holds what it held last time.
function usePrevious(value) {
const ref = React.useRef(undefined);
React.useEffect(() => { ref.current = value; }, [value]);
return ref.current; // during this render, still the PREVIOUS one
}
Passing a ref through your own component
Forward a ref only where it makes sense — a component wrapping a single input or button. A component that wraps a whole panel should expose behaviour through props instead.
// React 18 and earlier
const Field = React.forwardRef(function Field({ label, ...rest }, ref) {
return (
<p>
<label>{label}</label>
<input ref={ref} {...rest} />
</p>
);
});
// React 19: ref arrives as an ordinary prop, and forwardRef is no longer needed
function Field({ label, ref, ...rest }) {
return <input ref={ref} {...rest} />;
}
<Field ref={emailRef} label="Email" />
A callback ref
A function ref is called with the node on mount and with null on unmount — useful for a list of nodes, or when the node arrives and disappears conditionally.
<div ref={(node) => {
if (node) observer.observe(node);
return () => observer.unobserve(node); // React 19 supports a cleanup
}} />
// measuring as soon as the node exists
const [height, setHeight] = React.useState(0);
const measure = React.useCallback((node) => {
if (node) setHeight(node.getBoundingClientRect().height);
}, []);
Measuring without a flash
With useEffect the user sees one frame in the wrong place. With useLayoutEffect they never do — at the cost of blocking paint, so keep it small and use it only for measurement.
React.useLayoutEffect(() => {
const { height } = box.current.getBoundingClientRect();
setOffset(height + 8); // applied before the browser paints
}, [text]);