AniUI Academy

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.

9 min read

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 useful

The 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

Try it yourself
Loading playground...

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 ref as 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

up to 50
  1. 1.By default, if you pass a ref prop to a custom function component (not a plain host element like input), what happens historically (pre-React 19)?

  2. 2.What does React.forwardRef actually do?

  3. 3.What does useImperativeHandle let a component do that plain forwardRef alone doesn't?

  4. 4.What changed in React 19 regarding ref on function components?

4 left to answer