forwardRef and Forwarding Refs in Reusable Components
AI generated
{ }
React 19 · Refs · Component Libraries
forwardRef and Forwarding Refs
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.

13 min read Refs DOM Access Component APIs

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.

11. FAQ: forwardRef: The Key Facts at a Glance

1Why does a ref not work automatically on a custom-written component?
Because React treats props and ref as conceptually separate, and function components do not accept a ref by default. Only native host elements like input or div receive refs automatically.
2What exactly does forwardRef do?
forwardRef wraps a function component and gives it ref as a second parameter alongside props, so the component itself can decide which inner element to forward the ref to.
3Is forwardRef still needed in React 19, or is there a new API?
forwardRef remains the established mechanism for explicitly passing ref to a function component. For most wrapper components in libraries, it is still the standard approach.
4What is the difference between plain forwardRef and forwardRef combined with useImperativeHandle?
Plain forwardRef forwards the native DOM element entirely, including all DOM methods. useImperativeHandle restricts this and instead defines a custom object with only the methods the component deliberately wants to expose.
5When should I use useImperativeHandle instead of plain forwardRef?
When the component has internal React state that could become inconsistent with direct DOM access, or when only a limited, deliberately designed set of methods should be visible to the outside.
6Why do form libraries like React Hook Form need forwardRef-compatible components?
Such libraries use refs to register uncontrolled input fields directly and read their values without additional re-renders. Without forwardRef, they cannot obtain a ref to the inner input element.
7Does useImperativeHandle break React's normal data flow direction?
It deliberately deviates from the declarative, props-based data flow by providing an imperative API. That is justified for cases like setting focus or playing media, but it should not replace normal props-based data flow.
8Does every reusable component need forwardRef?
Not necessarily, only when there is a realistic need for consuming code to access the internal DOM element, for example for focus, measurements, or integration with ref-based libraries.
9What happens if I try to pass a ref to a component without forwardRef?
React logs a console warning that function components cannot receive refs by default, and the ref value stays null, causing any access through the ref to fail.
10Can useImperativeHandle hide several internal DOM elements behind a single ref API?
Yes, that is exactly one of its key benefits. A component can internally coordinate several refs to different DOM elements while still exposing only a single, consistent API to the outside through useImperativeHandle.