Learn useRef fundamentals: DOM access, callback refs, useImperativeHandle, and React 19 ref-as-prop.
useRef creates a plain JavaScript object with a single .current property. React keeps the same object across every render — mutating .current never schedules a re-render.
import { useRef } from 'react';
function TextInput() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus(); // direct DOM call
}
return (
<>
<input ref={inputRef} placeholder="Type here" />
<button onClick={handleClick}>Focus input</button>
</>
);
}function RenderCounter() {
const renders = useRef(0);
renders.current += 1; // mutate during render is fine for refs
return <p>Rendered {renders.current} times (no re-render loop)</p>;
}Before React 19, passing a ref into a child component required wrapping it in forwardRef. React 19 removes that requirement — ref is now a plain prop, just like className or onClick.
// React 18: you had to wrap in forwardRef
const Input = forwardRef(function Input(props, ref) {
return <input ref={ref} {...props} />;
});
function Form() {
const inputRef = useRef(null);
return <Input ref={inputRef} placeholder="Email" />;
}// React 19: ref is just a prop
function Input({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
function Form() {
const inputRef = useRef(null);
return <Input ref={inputRef} placeholder="Email" />;
}createRef and are unaffected by this change.Instead of passing a ref object to the ref prop, you can pass a function. React calls it with the DOM node on mount and null on unmount. This is useful when you need to act on the node the instant it appears — before any effect would fire.
function MeasuredBox() {
const [height, setHeight] = useState(null);
const measuredRef = useCallback((node) => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []); // stable function — no deps needed
return (
<>
<div ref={measuredRef} style={{ padding: 20 }}>
Resize me
</div>
{height !== null && <p>Height: {height}px</p>}
</>
);
}useCallback with a stable dependency array to avoid React re-running the ref function on every render.const logRef = useCallback((node) => {
if (node) {
console.log('mounted:', node.tagName);
} else {
console.log('unmounted');
}
}, []);
return <div ref={logRef}>Watch the console</div>;useImperativeHandle lets a child component decide exactly which methods a parent can call through a ref. Instead of exposing the raw DOM node, the child publishes a controlled object.
focus and clear without leaking the internal input ref:// React 19 style — ref as a prop, no forwardRef needed
function FancyInput({ ref }) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus() {
inputRef.current.focus();
},
clear() {
inputRef.current.value = '';
},
}));
return <input ref={inputRef} className="fancy" />;
}
function Form() {
const fancyRef = useRef(null);
return (
<>
<FancyInput ref={fancyRef} />
<button onClick={() => fancyRef.current.focus()}>Focus</button>
<button onClick={() => fancyRef.current.clear()}>Clear</button>
</>
);
}useImperativeHandle(ref, () => ({ getValue: () => value }), [value]);scrollIntoView, the parent simply cannot call it.useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus(),
// scrollIntoView intentionally not exposed
}));useState — not a ref.// Wrong: ref won't update the UI
const count = useRef(0);
const increment = () => { count.current++; }; // no re-render
// Right: state drives the UI
const [count, setCount] = useState(0);
const increment = () => setCount(c => c + 1);// Wrong: ref is null here
function Bad() {
const ref = useRef(null);
const width = ref.current?.offsetWidth; // always undefined on first render
return <div ref={ref}>{width}px</div>;
}
// Right: read in effect or event handler
function Good() {
const ref = useRef(null);
const [width, setWidth] = useState(0);
useEffect(() => { setWidth(ref.current.offsetWidth); }, []);
return <div ref={ref}>{width}px</div>;
}// Wrong: keeping a ref in sync with state manually
const doubleRef = useRef(count * 2);
// Right: derive during render
const double = count * 2;useState triggers a re-render when its value changes; useRef does not. Use useState when a value change needs to update the UI. Use useRef when you need to hold onto a DOM node, a timer ID, or any value that should survive re-renders without causing them.
No. In React 19, function components receive ref as a plain prop, so you can write function Input({ ref }) {} directly. forwardRef still works for backward compatibility but is considered legacy and may be removed in a future major version.
A callback ref is a function you pass to the ref prop instead of a ref object. React calls it with the DOM node on mount and with null on unmount. Use it when you need to imperatively measure an element or set up an observer as soon as the node becomes available, especially for dynamically rendered elements.
Use useImperativeHandle when a parent needs to call specific methods on a child component — for example, focus(), reset(), or scroll() — but you want to hide the child's internal DOM node. It lets you publish a minimal, intentional API instead of exposing the raw DOM element.
Refs attached to JSX elements are populated after the component mounts, so ref.current is null during the initial render. Always read ref.current inside a useEffect (which runs after mount) or inside an event handler (which fires after the user interacts with an already-mounted element).