loke.dev
Header image for Did React 19 Finally Kill the forwardRef Boilerplate?

Did React 19 Finally Kill the forwardRef Boilerplate?

React 19 streamlines component composition by treating 'ref' as a standard prop—here is how it simplifies your codebase and your TypeScript definitions.

· 4 min read

I’ve probably spent a cumulative week of my life just trying to remember exactly where the generic brackets go in a TypeScript forwardRef definition. It was one of those React API quirks that never quite felt "right"—a wrapper function that felt like a tax you had to pay just to access a DOM node from a parent.

With the release of React 19, that tax has finally been repealed. We can all stop pretending that ref is a special snowflake that deserves its own private entry point into a component. It's just a prop now.

The "Before" Times: The forwardRef Tax

To understand why this is such a win, we have to look at the ritual we used to perform. If you wanted to pass a ref to a child component, you couldn't just pass it like id or className. React would strip it out and give you a warning in the console.

Instead, you had to wrap your entire component in forwardRef:

// The React 18 way
import { forwardRef } from 'react';

const MyInput = forwardRef((props, ref) => {
  return (
    <div className="wrapper">
      <input {...props} ref={ref} />
    </div>
  );
});

MyInput.displayName = 'MyInput'; // Because forwardRef kills the component name

It wasn't the end of the world, but it was friction. It broke the mental model that "everything is a prop." Plus, if you forgot to set a displayName, your React DevTools would just show a sea of ForwardRef components, making debugging a nightmare.

React 19: Ref is Just a Prop

In React 19, ref has been demoted (or promoted, depending on your perspective) to a standard prop. You don't need the wrapper. You don't need the weird two-argument functional component signature.

Here is that same component today:

// The React 19 way
const MyInput = ({ label, ref, ...props }) => {
  return (
    <label>
      {label}
      <input {...props} ref={ref} />
    </label>
  );
};

That’s it. It’s a standard functional component. You pull ref out of the props object just like you would with children or color.

The TypeScript Glow-up

The real "chef's kiss" moment here is for TypeScript users. Typing a forwardRef was historically a bit of a mess. You had to remember that the generic order was <HTMLElement, PropType>, which is the opposite of almost every other React generic.

The old, painful way:

import { forwardRef, ComponentPropsWithoutRef } from 'react';

type InputProps = ComponentPropsWithoutRef<'input'> & {
  label: string;
};

const MyInput = forwardRef<HTMLInputElement, InputProps>(
  ({ label, ...props }, ref) => {
    return (
      <label>
        {label}
        <input {...props} ref={ref} />
      </label>
    );
  }
);

The React 19 way:

type InputProps = {
  label: string;
  ref?: React.Ref<HTMLInputElement>;
} & React.ComponentProps<'input'>;

const MyInput = ({ label, ref, ...props }: InputProps) => {
  return (
    <label>
      {label}
      <input {...props} ref={ref} />
    </label>
  );
};

It’s much more readable. We are just defining a prop named ref that expects a specific type of reference. No more jumping through hoops to make the compiler happy.

What about useImperativeHandle?

I know what a few of you are thinking: "What happens to the fancy stuff?"

If you're using useImperativeHandle to expose custom methods to a parent (like ref.current.focusAndShake()), the pattern stays almost identical. The only difference is that you pass the ref prop as the first argument to the hook.

import { useImperativeHandle, useRef } from 'react';

const FancyInput = ({ ref }) => {
  const inputRef = useRef(null);

  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
    scrollIntoView: () => {
      inputRef.current.scrollIntoView();
    }
  }));

  return <input ref={inputRef} />;
};

Are there any gotchas?

The main thing to keep in mind is the transition period. While forwardRef is effectively deprecated in React 19, it hasn't been deleted. Your existing code won't suddenly explode.

However, if you are building a library that needs to support both React 18 and React 19, you'll likely need to stick with forwardRef for a little while longer to ensure backward compatibility.

Also, if you're using a class component... well, first of all, why? But second, ref as a prop doesn't work there because class components handle refs differently via the ref attribute on the instance. But let's be honest, if you're writing React 19 code, you're probably staying in function-land.

The Verdict

Is forwardRef dead? For new projects, yes.

React 19 successfully removes one of the most common "paper cuts" in the API. By treating ref as a prop, the team has made components more predictable and significantly reduced the boilerplate required for clean, reusable UI kits.

It's a small change in syntax, but a huge upgrade for our collective sanity. Go forth and delete those wrappers.