Lesson 28 of 31
forwardRef and Ref as a Prop
Exposing a DOM node (or a custom API) from inside a component — the forwardRef wrapper, useImperativeHandle for controlling what's exposed, and React 19's simpler ref-as-a-prop model.
The useRef lesson covered getting a ref to a plain host element like
<input ref={inputRef} />. This lesson covers the trickier case: exposing a
DOM node (or a deliberately narrow API) from inside a reusable component
you wrote yourself.
Why this needs anything special at all
A ref passed to a built-in element like input or div is handled
specially by React — it always resolves to the real DOM node. A ref passed
to a component you wrote is different: historically, a plain function
component simply had no defined behavior for a ref prop at all, since
ref, like key, is handled specially by React rather than arriving as a
normal prop.
function TextInput(props) {
return <input {...props} />;
}
const inputRef = useRef(null);
<TextInput ref={inputRef} />; // historically: ref goes nowhere usefulThe historical fix: forwardRef
forwardRef wraps a component so it receives an explicit second argument
— the ref itself — which it can then attach to whatever DOM node inside it
should actually receive it:
import { forwardRef } from "react";
const TextInput = forwardRef(function TextInput(props, ref) {
return <input ref={ref} {...props} />;
});
function Form() {
const inputRef = useRef(null);
return (
<>
<TextInput ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>Focus</button>
</>
);
}Now inputRef.current in Form resolves to the actual <input> DOM node
rendered inside TextInput, exactly as if Form had rendered that input
directly.
Exposing a narrower API with useImperativeHandle
Sometimes exposing the entire raw DOM node is more than a component wants
to hand out — useImperativeHandle lets it substitute a custom object
instead:
import { forwardRef, useImperativeHandle, useRef } from "react";
const TextInput = forwardRef(function TextInput(props, ref) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus() {
inputRef.current.focus();
},
clear() {
inputRef.current.value = "";
},
}));
return <input ref={inputRef} {...props} />;
});Now inputRef.current in the parent only has .focus() and .clear() —
not arbitrary DOM access like .value or .style. This is a deliberate
design choice: it lets a component expose a small, intentional imperative
API while keeping the rest of its internals private, the same discipline as
choosing what to export from a module.
React 19: ref as an ordinary prop
React 19 simplified the common case: a function component can now declare
ref as a regular parameter, right alongside its other props, without
forwardRef at all:
function TextInput({ ref, ...props }) {
return <input ref={ref} {...props} />;
}This removes the extra wrapping step for the common "just forward this ref
to one DOM node inside" case. forwardRef still works unchanged for
existing code (there's no need to rewrite components that already use it),
and useImperativeHandle is still exactly how you'd expose a custom API
rather than the raw node, in either style.
Try it yourself
What to remember
- ref isn't automatically forwarded to a DOM node inside a custom function component — historically it required forwardRef to receive and attach it explicitly.
- useImperativeHandle lets a component expose a narrow, custom API (like just
.focus()) instead of the raw underlying DOM node. - React 19 allows a function component to accept
refas an ordinary prop directly, without forwardRef — existing forwardRef code keeps working unchanged. - Either way, the underlying idea is the same: giving a parent controlled, intentional access to something imperative living inside a child component.
Check yourself
4 questions · pass 3/4 to unlock Suspense for Data Fetching
1.By default, if you pass a
refprop to a custom function component (not a plain host element likeinput), what happens historically (pre-React 19)?2.What does React.forwardRef actually do?
3.What does useImperativeHandle let a component do that plain forwardRef alone doesn't?
4.What changed in React 19 regarding ref on function components?
4 left to answer