React.js, part 14: ref as a prop, ref cleanup, and the refs you should stop writing
Part 14from the React.js series · 20 parts in all
For years passing a ref through a component meant wrapping it in forwardRef —
a ceremony with no equivalent for any other prop. React 19 removed it: ref is
now an ordinary prop. Combined with cleanup functions returned from ref callbacks, this closed
a quiet memory-leak source. Part 14 is the short, satisfying cleanup.
ref is a prop now
// Before: a whole wrapper just to punch a hole through the component.
const Field = forwardRef(function Field(props, ref) {
return <input ref={ref} {...props} />;
});
// Now: ref arrives like any other prop.
function Field({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
// Usage is unchanged.
<Field ref={inputRef} placeholder="Email" />
forwardRef still works and is not going anywhere soon, but new code does not
need it, and a component can now spread ref the way it spreads the rest of its
props — which fixes a long-standing inconsistency with component libraries.
Ref callbacks can return a cleanup
Historically a ref callback that set up an observer returned nothing, and the only
teardown point was calling the callback again with null. React 19 supports the
same shape effects use — return a function:
function Tooltip({ text }) {
const cleanupRef = (node) => {
if (!node) return; // React may still call with null
node.classList.add('has-tooltip');
node.setAttribute('title', text);
// Returned cleanup runs when the node detaches OR the ref changes.
return () => {
node.classList.remove('has-tooltip');
node.removeAttribute('title');
};
};
return <span ref={cleanupRef}>{text}</span>;
}
That symmetry closes the gap where a ref callback attached a listener or an
IntersectionObserver and the teardown was a separate, easily forgotten
null branch.
The refs to stop writing
- Refs to hold derived data. If it can be computed from props or state, it is not a ref.
- Refs that mirror state for "latest value". In event handlers this is
usually
useEffectEvent(part 17); reach for a ref only when you truly need a mutable box inside effects. - Refs read during render. Reading
ref.currentin the render body is non-deterministic under concurrency; keep reads in effects and handlers.
Next: document metadata and stylesheets, which React 19 now hoists for you instead of requiring a helmet library.