In reusable components
A ref on a custom-written component does not work by default, because React does not automatically forward refs across component boundaries. forwardRef and useImperativeHandle solve this deliberately, without giving up the component's encapsulation.
Table of Contents
- 1. The core problem: refs stop at the component boundary
- 2. How the problem shows up without forwardRef
- 3. The fix: forwardRef makes the ref explicitly controllable
- 4. Typical use cases in component libraries
- 5. When direct DOM access exposes too much
- 6. useImperativeHandle for a controlled, custom ref API
- 7. Coordinating multiple refs inside a single component
- 8. When it is better to avoid forwardRef
- 9. A summarizing comparison of the three approaches
- 10. Summary
- 11. FAQ
1. The core problem: refs stop at the component boundary
A ref passed directly to a native DOM element like <input /> always works, because React automatically attaches refs on native host elements to the underlying DOM node. Pass that same ref to a custom-written function component, for example a reusable TextField component, and it simply does not work: React has no built-in mechanism for function components to automatically forward a ref to an inner element.
The reason is that props and ref are conceptually treated separately in React. A function component does not simply receive ref as another entry in the props object, because that would blur the meaning of refs, any component could otherwise do arbitrary, opaque things with a ref disguised as a prop. Instead, a component must explicitly signal whether and how it forwards refs inward.
2. How the problem shows up without forwardRef
If you try to pass a ref directly to a plain function component, React logs a console warning that function components cannot receive refs by default, and the ref stays null. For a component that internally renders a focusable <input>, this means the calling side has no way to programmatically focus that field, even though that is exactly a legitimate, common use case.
This failure mode is especially relevant in form-heavy applications, where, for example, the first invalid field should automatically be focused after a validation error message. Without a way to forward the ref deliberately, this logic would either have to be solved entirely inside the wrapper component itself, limiting reusability, or the component's encapsulation would have to be abandoned.
function TextField(props) {
return <input {...props} />;
}
function Form() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current?.focus(); // does NOT work
}, []);
// Warning: Function components cannot be given refs
return <TextField ref={inputRef} />;
}
3. The fix: forwardRef makes the ref explicitly controllable
forwardRef() wraps a function component and gives it a second parameter alongside props, namely ref. Inside the component, you then decide yourself which inner element that ref gets forwarded to, usually the native DOM element the component wraps. That makes the calling side behave exactly as expected from a native element.
It is important that forwardRef does not remove the component's encapsulation but deliberately opens it: the component actively decides which internal element is reachable through the ref, not the calling side. This fundamentally distinguishes forwardRef from direct DOM access via document.querySelector, which would bypass the component's encapsulation entirely.
const TextField = forwardRef(function TextField(props, ref) {
return <input ref={ref} {...props} />;
});
function Form() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current?.focus(); // now works correctly
}, []);
return <TextField ref={inputRef} />;
}
4. Typical use cases in component libraries
In reusable UI component libraries, forwardRef is nearly standard practice, because users of such libraries expect their own wrapper components to behave like native elements. A Button, a TextField, or a Select from a design system library needs to accept a ref so that consuming applications can set focus, take measurements with getBoundingClientRect(), or interact with animation libraries that require direct DOM access.
Form libraries such as React Hook Form also internally rely on forwardRef-compatible input components, because they use refs to register uncontrolled input fields directly and read their values without additional re-renders. A custom input component that does not forward a ref cannot easily be integrated with such libraries.
5. When direct DOM access exposes too much
Simply forwarding the DOM element via forwardRef gives the calling side full access to every native DOM API method, such as focus(), blur(), remove(), or direct style manipulation. For simple wrappers, this is usually unproblematic, but for more complex, stateful components it can hand out too much control and undermine internal encapsulation.
One example is a VideoPlayer component that internally coordinates several DOM elements and React state. If the raw <video> element were exposed directly via ref, the calling side could call methods like load() without the component's internal React state ever finding out, which can lead to inconsistencies between visible DOM state and React state.
6. useImperativeHandle for a controlled, custom ref API
useImperativeHandle solves this by defining, inside the component, exactly which object becomes visible through the forwarded ref, instead of automatically exposing the raw DOM element. You combine an internal ref to the native element with a second, public ref passed in from outside, and explicitly define an object with only the methods the calling side is allowed to use.
In the example below, the calling side gets access, through the ref, only to play() and pause(), both implemented so they also correctly update the component's internal React state. Direct access to the raw <video> element or other DOM methods remains blocked, which lets the component guarantee its internal consistency while still offering a convenient, imperative API to the outside.
const VideoPlayer = forwardRef(function VideoPlayer(props, ref) {
const videoRef = useRef(null);
const [isPlaying, setIsPlaying] = useState(false);
useImperativeHandle(ref, () => ({
play() {
videoRef.current.play();
setIsPlaying(true);
},
pause() {
videoRef.current.pause();
setIsPlaying(false);
},
}), []);
return <video ref={videoRef} src={props.src} />;
});
// Usage
function App() {
const playerRef = useRef(null);
return (
<>
<VideoPlayer ref={playerRef} src="/demo.mp4" />
<button onClick={() => playerRef.current.play()}>Play</button>
</>
);
}
7. Coordinating multiple refs inside a single component
More complex components often need to coordinate several internal refs, for example a container element and a focusable input element inside a composite component like a search widget with a dropdown. In such cases, useImperativeHandle defines a public API that internally accesses several different DOM refs but exposes only a single, consistent interface to the outside.
This encapsulation is an important design benefit over simply passing through a single DOM element: the component's internal structure can change, for example when an additional wrapper element gets introduced, without the public ref API, and thus the contract with consuming components, having to change.
8. When it is better to avoid forwardRef
Not every component needs forwardRef. For components used exclusively inside your own application and never needing external DOM access, forwardRef is unnecessary complexity. Similarly, for purely presentational components without a focusable or measurable inner element, a ref is often not meaningfully usable at all.
The rule of thumb is: as soon as a component is part of a reusable library or needs to interact with libraries that themselves rely on refs, such as form or animation libraries, forwardRef is almost always worthwhile. For internal, application-specific code, the investment usually only pays off once a concrete use case for external focus or measurement access arises.
9. A summarizing comparison of the three approaches
In summary, there are three distinct levels of ref forwarding, each with a different degree of control and encapsulation. Native elements receive refs automatically, without any intervention. forwardRef forwards full DOM access, in a controlled way, to exactly one inner element. useImperativeHandle instead defines a deliberately restricted, custom API that exposes only selected methods to the outside.
The table below compares the three approaches and their respective properties, to make the decision for a concrete use case easier.
| Approach | Level of control | Typical use case | Encapsulation |
|---|---|---|---|
| Ref on a native element | full DOM access automatically | simple HTML elements | none |
| forwardRef without useImperativeHandle | full DOM access, forwarded deliberately | simple wrapper components | low |
| forwardRef with useImperativeHandle | only defined methods visible | complex, stateful components | high |
| No ref support | no external DOM access | purely internal presentational components | complete |
Mironsoft
React architecture, performance, and Magento frontend integration
React frontends that stay fast instead of slowing down with every feature?
We review existing React applications for unnecessary re-renders, bloated bundles, and fragile state management, then build a frontend that stays performant and connects cleanly to Magento or other backends.
Performance Audit
Systematically measuring and fixing re-renders, bundle size, and load times.
State Architecture
Cleanly separating context, client state, and server state instead of mixing everything.
Magento Integration
Building robust, type-safe GraphQL or REST integration with Magento.
10. Summary
forwardRef: The Key Facts at a Glance
Core problem
Refs are not forwarded to inner elements by default in function components.
Basic fix
forwardRef explicitly forwards the ref parameter to an inner element.
Fine control
useImperativeHandle defines a custom, restricted ref API instead of full DOM access.
Typical use
Reusable component libraries and integration with form/animation libraries.