Forwarding Refs
Refs let you reach a DOM node imperatively — to focus an input, scroll an element into view, or measure its size. But when you attach a ref to your own component instead of a built-in element, it does not “just work”: React intercepts the ref and there is no DOM node to give back. Forwarding refs is the mechanism that lets a parent component reach through your component and grab the inner DOM node it actually cares about. This page covers why refs do not pass through by default, how to forward them, how to expose a custom imperative handle, and how the React 19 ref-as-prop change simplifies everything.
Why refs do not pass through by default
ref is a reserved prop, like key. When you render <MyInput ref={inputRef} />, React does not place inputRef inside MyInput’s props object. Instead it tries to attach the ref to the component instance — and function components have no instance. The result is that inputRef.current stays null.
function MyInput(props) {
// props.ref is undefined here in React 18 — ref was consumed by React.
return <input className="field" {...props} />;
}
function Form() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus(); // 💥 inputRef.current is null
}
return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>Focus</button>
</>
);
}
The parent wants to focus the underlying <input>, but the ref never reaches it.
Forwarding to an inner input
The classic solution (React 16.3–18) is forwardRef. It wraps your component so React passes the incoming ref as a second argument, which you then attach to the real DOM node.
import { forwardRef, useRef } from "react";
const MyInput = forwardRef(function MyInput(props, ref) {
return <input ref={ref} className="field" {...props} />;
});
function Form() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus(); // ✅ now points to the <input>
}
return (
<>
<MyInput ref={inputRef} placeholder="Email" />
<button onClick={handleClick}>Focus</button>
</>
);
}
Now inputRef.current is the actual <input> element, and focus() works. The component author decides which inner node the ref binds to, keeping the rest of the DOM structure private.
Forwarding gives the parent access to a specific node you choose to expose — not your whole component. Treat the forwarded ref as a deliberate part of your public API.
Combining with useImperativeHandle
Sometimes you do not want to hand over the raw DOM node. Maybe you want to expose only focus() and a custom scrollIntoView(), while hiding everything else. useImperativeHandle lets you define exactly what ref.current becomes.
import { forwardRef, useImperativeHandle, useRef } from "react";
const SearchField = forwardRef(function SearchField(props, ref) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus() {
inputRef.current.focus();
},
clear() {
inputRef.current.value = "";
inputRef.current.focus();
},
}), []);
return <input ref={inputRef} type="search" {...props} />;
});
function Toolbar() {
const fieldRef = useRef(null);
return (
<>
<SearchField fieldRef={undefined} placeholder="Search…" ref={fieldRef} />
<button onClick={() => fieldRef.current.focus()}>Focus</button>
<button onClick={() => fieldRef.current.clear()}>Clear</button>
</>
);
}
The parent can call fieldRef.current.focus() and fieldRef.current.clear(), but it cannot reach the underlying <input> directly. This is the recommended way to expose a constrained imperative API.
The React 19 ref-as-prop change
React 19 removes the need for forwardRef in most cases. ref is now a regular prop that function components receive like any other. You destructure it directly — no wrapper required.
import { useRef } from "react";
// React 19: ref arrives as a normal prop.
function MyInput({ ref, ...props }) {
return <input ref={ref} className="field" {...props} />;
}
function Form() {
const inputRef = useRef(null);
return (
<>
<MyInput ref={inputRef} placeholder="Email" />
<button onClick={() => inputRef.current.focus()}>Focus</button>
</>
);
}
useImperativeHandle still works the same way — you just pass the ref prop into it instead of a forwarded argument.
function SearchField({ ref, ...props }) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus(),
}), []);
return <input ref={inputRef} type="search" {...props} />;
}
forwardRef is not removed in React 19 — existing code keeps working — but it is deprecated and will be cleaned up in a future major version. A codemod is provided to migrate automatically.
TypeScript signatures
import { useRef } from "react";
interface MyInputProps extends React.ComponentPropsWithoutRef<"input"> {
ref?: React.Ref<HTMLInputElement>;
}
function MyInput({ ref, ...props }: MyInputProps) {
return <input ref={ref} {...props} />;
}
API comparison
| Approach | React version | How ref arrives | Best for |
|---|---|---|---|
| Not forwarded | any | not available | components that never expose a node |
forwardRef | 16.3+ | second function argument | libraries supporting React 18 and below |
| ref-as-prop | 19+ | normal prop | new code on React 19 |
useImperativeHandle | 16.8+ | wraps either of the above | exposing a custom, limited API |
Output:
forwardRef → (props, ref) => <input ref={ref} />
React 19 → ({ ref, ...props }) => <input ref={ref} />
Best practices
- Forward refs from reusable, leaf-like components (inputs, buttons, custom controls) so consumers can integrate with focus management and form libraries.
- Forward to exactly one meaningful DOM node; if there are several, prefer
useImperativeHandleto expose a clear, named API. - Use
useImperativeHandleto hide implementation details — return only the methods callers actually need. - On React 19, write new components with
refas a plain prop and skipforwardRefentirely. - When publishing a library that must support React 18, keep
forwardReffor backward compatibility. - Avoid using refs to do what props and state can do; reach for forwarding only when you genuinely need imperative DOM access.
- Always null-check
ref.currentbefore calling methods, since it isnullbefore mount and after unmount.