React useRef Hook
Learn useRef fundamentals: DOM access, callback refs, useImperativeHandle, and React 19 ref-as-prop.
TL;DR
- 01useRef persists a .current value across renders without re-rendering.
- 02React 19 passes ref as a prop — forwardRef is deprecated.
- 03useImperativeHandle lets a child component expose a controlled API.
Tips
- 01Initialise useRef with the type of value you expect. Pass null for DOM refs and a real initial value (e.g. 0, false) for mutable value refs — this makes TypeScript inference easier.
Warnings
- 01A ref mutation inside render is allowed only for the renders counter pattern shown above where you don't use the value for display. Any ref read that feeds JSX output must happen in an effect.
useRef Basics
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.
- Attach a ref to a DOM element to call browser methods on it.
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> </> ); } - Store any mutable value — not just DOM nodes. Here a render counter persists without triggering re-renders.
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>; } - Key distinction: ref vs state.
- ref.current changes — component does not re-render, UI stays the same.
- setState changes — component re-renders, UI updates.
React 19: Refs as Props
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.
- Before (React 18 and earlier) — forwardRef wrapper required:
// 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" />; } - After (React 19) — ref arrives as a plain prop:
// 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" />; } - Class components still use
createRefand are unaffected by this change. - forwardRef still works in React 19 for library backward compatibility but shows a deprecation warning in dev mode.
Callback Refs
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.
- Measure an element's size the moment it mounts:
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>} </> ); } - The callback fires again when the element re-mounts (e.g. after conditional rendering), giving you an automatic measurement update — a regular ref object would not trigger this.
- Wrap the callback in
useCallbackwith a stable dependency array to avoid React re-running the ref function on every render. - Clean-up pattern — run code on both attach and detach:
const logRef = useCallback((node) => { if (node) { console.log('mounted:', node.tagName); } else { console.log('unmounted'); } }, []); return <div ref={logRef}>Watch the console</div>;
useImperativeHandle
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.
- Expose
focusandclearwithout 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> </> ); } - Pass a dependency array as the third argument to control when the handle object is recreated:
useImperativeHandle(ref, () => ({ getValue: () => value }), [value]); - Restrict the exposed API intentionally — if you don't expose
scrollIntoView, the parent simply cannot call it.useImperativeHandle(ref, () => ({ focus: () => inputRef.current.focus(), // scrollIntoView intentionally not exposed }));
Best Practices
- Prefer state over refs for UI values. If a value change should update the screen, use
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); - Never read refs during rendering. The DOM node is null until after mount. Reading it during render returns null at best and throws at worst.
// 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>; } - React 19: drop forwardRef in new components. Accept ref as a plain prop — it's simpler and the JSX tools generate better TypeScript types automatically.
- Use callback refs for dynamic elements. When an element may mount and unmount (e.g. behind a conditional), a callback ref ensures you always run your setup code at the right time.
- Don't store derived data in refs. Compute it during render instead — refs that shadow state become stale and are hard to debug.
// Wrong: keeping a ref in sync with state manually const doubleRef = useRef(count * 2); // Right: derive during render const double = count * 2;
FAQ
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).