Callback Refs, Ref Forwarding, and Imperative Handles
Learn how callback refs run custom logic on attach and detach, how refs are passed through custom components (including the legacy forwardRef wrapper and the modern ref-as-prop pattern), and how useImperativeHandle exposes a deliberate, minimal API instead of a raw DOM node.
useRef on its own only gets you a ref to an element you render directly. Real applications also need to pass refs down into custom components, run logic exactly when a node is attached or removed, and sometimes expose something other than the raw DOM node to whoever holds the ref. Callback refs, ref forwarding, and useImperativeHandle are the three tools that cover those cases, and understanding when to reach for each one is what separates ad hoc DOM hacking from a deliberate ref architecture.
Callback Refs: Running Code on Attach and Detach
Instead of passing a ref object created by useRef, you can pass a function directly as the ref prop: <div ref={node => { /* ... */ }} />. React calls that function with the DOM node when the element mounts, which lets you run setup logic — like measuring the node or starting a ResizeObserver — at the exact moment it becomes available, without waiting for a separate effect to run afterward.
In the classic pattern, React calls the same callback ref function again with null when the element unmounts, and it's the callback's job to handle both cases (attach when it receives a node, detach when it receives null). In React 19 and later, a callback ref can instead return its own cleanup function — much like an effect does — and React calls that returned function automatically on detach instead of calling the ref function again with null.
Callback refs are especially useful for measuring an element as soon as it appears (rather than one render cycle later via an effect) and for attaching refs to items in a dynamically sized list, where a single useRef object wouldn't make sense because there's no fixed number of elements to track.
Passing Refs Through Components (Ref Forwarding)
A ref attached to a custom component — <MyInput ref={inputRef} /> — doesn't automatically reach any DOM node inside MyInput, because MyInput is just a function, not an element. In older versions of React, making that ref reach the inner <input> required explicitly wrapping the component definition in forwardRef(), which gave the component a second parameter for the incoming ref alongside props.
This forwardRef wrapper is legacy machinery: as of modern React, function components can accept ref as an ordinary prop, the same way they accept any other prop, with no wrapping function required. You destructure it out of the props object and attach it wherever you want inside the component's JSX, exactly like you would with any other DOM ref.
Either way, the underlying goal is the same — letting a parent component reach past a child's boundary to interact with something inside it — but the modern approach removes an entire extra API surface (forwardRef, plus its slightly different generic-typing rules) that used to be required for something as simple as 'let the parent focus my internal input.'
Imperative Handles: Exposing a Custom API
Forwarding the raw DOM node works, but it also hands the parent unrestricted access to every native DOM method — the parent could call inputRef.current.remove() and delete the element outright, which is rarely what a component author wants to allow. useImperativeHandle lets a component control exactly what a parent-held ref sees: instead of exposing the DOM node itself, the component builds a small custom object with only the methods it wants to allow, such as focus() or scrollToTop(), and useImperativeHandle attaches that object to the ref instead.
This keeps the internal DOM structure as an implementation detail. The component's author is free to change what's rendered inside — restructure the markup, swap an <input> for a styled wrapper — without breaking any parent that only ever called the small, explicit methods the imperative handle exposed.
useImperativeHandle should be reached for deliberately, not by default: most components should never need it, since regular props (values and callback props) already cover the vast majority of parent-child communication in React. It exists specifically for the narrow slice of interactions — focus, scroll, play/pause, open/close — that are genuinely imperative actions rather than data flowing through props.
Designing Ref Architecture
A good rule for any component library is to keep the imperative surface as small as possible. Props and state should carry all the data and configuration a component needs; refs should only be reached for when there's a genuinely imperative action to trigger — one that doesn't map cleanly onto 'this prop changed, re-render accordingly.'
Overusing refs to sidestep React's normal data flow — reading a child's internal state through a ref instead of lifting that state up, or triggering a re-render by mutating a ref and forcing an update — usually signals a design problem rather than a ref problem. When a value or an action genuinely belongs in the render output, it should flow through props and state, with useImperativeHandle reserved for actions a parent needs to trigger on demand outside the normal render cycle.
useImperativeHandle with a modern ref-as-prop component
The parent's ref only ever sees { play, pause } — never the raw <video> element or any other DOM method. This example uses the modern pattern where VideoPlayer accepts ref directly as a prop, with no forwardRef wrapper.
type VideoHandle = { play: () => void; pause: () => void };
function VideoPlayer({ src, ref }: { src: string; ref: React.Ref<VideoHandle> }) {
const videoRef = useRef<HTMLVideoElement>(null);
useImperativeHandle(ref, () => ({
play: () => videoRef.current?.play(),
pause: () => videoRef.current?.pause(),
}));
return <video ref={videoRef} src={src} />;
}Code Examples
forwardRef takes the component function and gives it a second argument, ref, separate from props. This was required in older React versions any time a function component needed to accept and forward a ref, and you'll still see it throughout existing codebases and libraries.
Common mistake: A component that receives a ref prop without being wrapped in forwardRef (on older React versions) simply ignores it — the ref stays null and no error is thrown, which makes the bug easy to miss.
In modern React, ref is just another named prop that a function component can declare and use however it likes — here it's passed straight through to the inner <input>. There's no separate wrapper function and no special second argument to remember.
function FancyInput({ label, ref }: { label: string; ref: React.Ref<HTMLInputElement> }) {
const FancyInput = forwardRef<HTMLInputElement, { label: string }>(
function FancyInput({ label }, ref) {
return (
<label>
{label}
<input ref={ref} />
</label>
);
}
);
// Usage: <FancyInput ref={inputRef} label="Name" />function FancyInput({ label, ref }: { label: string; ref: React.Ref<HTMLInputElement> }) {
return (
<label>
{label}
<input ref={ref} />
</label>
);
}
// Usage is identical: <FancyInput ref={inputRef} label="Name" />Ref forwarding vs imperative handle
Plain forwarding exposes everything the DOM node can do; useImperativeHandle exposes only a deliberate, minimal surface.
Parent Child component Exposed via ref
-------- ---------------- ---------------
ref (raw) ---> passes ref to <input> ---> full HTMLInputElement (all native methods)
ref (handle)---> useImperativeHandle ---> { play, pause } (only the methods the child chose)Important Rules
Prefer letting a function component accept ref directly as a prop over wrapping it in forwardRef, unless the codebase targets an older React version that requires forwardRef.
Use a callback ref (rather than useRef plus an effect) when you need to react at the exact moment an element attaches or detaches, such as starting a ResizeObserver.
Reach for useImperativeHandle only when a parent genuinely needs to trigger an imperative action on a child — never as a substitute for passing data through props.
Keep any imperative handle's exposed API as small and intention-revealing as possible (play/pause, focus/blur, open/close) rather than exposing the underlying DOM node wholesale.
Where this shows up
Reusable UI kit components — custom text inputs, modals, video players, toast notifications — routinely need to expose a small imperative API (focus(), open(), play()) so consuming code can trigger an action on demand, while keeping their internal DOM structure free to change. Design systems built as npm packages lean on this pattern heavily, since it lets the library evolve its internal markup across versions without breaking every consumer that holds a ref to one of its components.
function Modal({ ref }: { ref: React.Ref<HTMLDivElement> }) { return <div ref={ref} className="modal">...</div>; } // Parent: modalRef.current.remove(); // deletes the modal from the DOM directly function Modal({ ref }: { ref: React.Ref<{ close: () => void }> }) { const divRef = useRef<HTMLDivElement>(null); useImperativeHandle(ref, () => ({ close: () => divRef.current?.classList.add("closing"), })); return <div ref={divRef} className="modal">...</div>; }
Exposing the raw DOM node lets any consumer call arbitrary native methods on it, including ones that break the component's internal invariants (like removing it from the DOM outside of React's own lifecycle). An imperative handle limits the parent to the one sanctioned action, close(), that the component author intended to support.
[concept] What is the main advantage of useImperativeHandle over forwarding the raw DOM node? A. It runs faster than a plain ref B. It lets the component expose only a deliberate, minimal API instead of the full native element C. It is required for TypeScript to type the ref correctly D. It automatically re-renders the parent whenever the child changes
B. It lets the component expose only a deliberate, minimal API instead of the full native element — useImperativeHandle replaces what a ref exposes with a custom object of your choosing, so the parent can only call the specific methods the component author intended to allow, rather than every native DOM method.
[concept] In modern React, how does a function component accept an incoming ref without forwardRef? A. It cannot — forwardRef is always required B. It declares ref as a regular named prop, like any other prop C. It calls useRef() twice D. It uses useImperativeHandle instead of a ref prop
B. It declares ref as a regular named prop, like any other prop — Modern React allows function components to receive ref as an ordinary prop alongside their other props, eliminating the need for the forwardRef wrapper that older versions required.
[predict-output] A callback ref function is passed as ref={node => console.log(node)} on a <div>. What gets logged when the div first mounts, and again when it unmounts (assuming the classic two-call pattern, not a returned cleanup function)? A. The div element, then undefined B. The div element, then null C. null, then the div element D. The div element is logged only once, on mount
B. The div element, then null — React calls a callback ref with the actual DOM node on mount, then calls it again with null on unmount (unless the callback instead returns its own cleanup function, in which case React calls that instead of calling the ref function again).
Build a Timer component that renders elapsed seconds and exposes a handle with reset() (sets the count back to 0) via useImperativeHandle, without exposing any DOM node to the parent.
type TimerHandle = { reset: () => void }; function Timer({ ref }: { ref: React.Ref<TimerHandle> }) { const [seconds, setSeconds] = useState(0); useEffect(() => { const id = setInterval(() => setSeconds(s => s + 1), 1000); return () => clearInterval(id); }, []); useImperativeHandle(ref, () => ({ reset: () => setSeconds(0), })); return <p>{seconds}s</p>; }
Store the seconds in state, keep an interval running in an effect, and let useImperativeHandle's factory function call setSeconds(0) for reset().