I have a habit I’m not proud of. Every time I needed to pass a ref into one of my own components, I’d pause, sigh, and go copy forwardRef out of another file, because I could never remember the exact shape of it. Was the ref the first argument or the second? Did the generic go on forwardRef or on the function? I wrote React for years and never internalized it. I just kept a working example open in another tab.
React 19 quietly fixed this, and it fixed it by deleting the problem instead of documenting it better. ref is now a regular prop. You read it like any other value in props. forwardRef still works, but it’s on the way out, and the replacement is small enough that I went back through an old component library and removed it everywhere in an afternoon.
Here’s what actually changed, what the migration looks like, and the edge cases worth knowing about before you start.
The thing forwardRef was working around
For most of React’s life, ref was not a normal prop. If you wrote <MyInput ref={something} />, React intercepted that ref and never passed it through. Your function component simply could not see it. This was deliberate, and for class components it even made some sense, but for function components it meant a parent had no way to reach a child’s DOM node without help.
forwardRef was that help. It wrapped your component and handed you the ref as a second argument, kept separate from props:
import { forwardRef } from "react";
const TextField = forwardRef(function TextField(props, ref) {
return <input ref={ref} className="field" {...props} />;
});
It works. It has always worked. But look at it for a second. The ref is pulled out of the normal argument list and parked in its own slot. If you used TypeScript, you also fed forwardRef two type parameters in an order I looked up every single time. The component got a wrapper, the wrapper introduced a display-name quirk in DevTools, and the whole construct existed to route one value around a rule that only applied to function components by an accident of history.
The worst part was the failure mode. When you forgot the wrapper, nothing stopped you. You’d pass a ref to a plain function component, React would print a console warning that function components can’t be given refs, and ref.current would simply stay null. I lost time to that warning more than once, usually right after refactoring a class component into a function and forgetting that the rewrite had quietly dropped its ability to receive a ref.
What React 19 changed
In React 19, ref is just a prop. You destructure it like anything else:
function TextField({ ref, ...props }) {
return <input ref={ref} className="field" {...props} />;
}
That’s the whole component. No wrapper, no second argument, no import. The parent doesn’t change at all, it still writes <TextField ref={inputRef} />. The entire difference is on the definition side, and the definition lost a line and an import.
TypeScript gets cleaner too. Instead of forwardRef<HTMLInputElement, Props>, you put the ref in your props type where it belongs:
type Props = {
ref?: React.Ref<HTMLInputElement>;
label: string;
};
function TextField({ ref, label }: Props) {
return (
<label>
{label}
<input ref={ref} />
</label>
);
}
The ref is now a prop with a type, sitting next to the other props and read the same way. I lean on the type system hard, for reasons I went into in a post on TypeScript generics in production, and this change fits that thinking. It removes a special case the compiler used to need a separate helper for.
forwardRef itself isn’t gone. It still runs in React 19. But the React 19 release notes mark it as deprecated, and the forwardRef reference page now opens by telling you to read ref as a prop instead. A later version will remove it outright, so new components may as well skip it.
Migrating without breaking things
You don’t have to convert everything at once, and you shouldn’t try to in a single commit. A component using forwardRef and a component reading ref as a prop are interchangeable from the caller’s side. Nothing downstream notices the difference. So you can migrate one file at a time and ship each change on its own.
The mechanical edit is small. Remove the forwardRef import. Remove the wrapper call. Move ref from the second parameter into the destructured props. If you had a manual displayName assignment that only existed because forwardRef components showed up oddly in React DevTools, you can usually delete that as well, since a plain named function already carries its name.
React ships a codemod for the bulk of it. The react-codemod package has a transform that rewrites forwardRef calls into the prop form. I ran it on a component library of about forty files. It got roughly thirty-five right with no help from me. The five it left untouched were the interesting ones, and they were interesting for a reason worth understanding.
The codemod is conservative. When a component does something unusual with its ref, it backs off rather than guessing wrong. Treat it as the first pass, not the whole job. Read every file it changed, and read every file it refused to change, because that second list is your actual work.
The edge cases that bit me
Three things slowed me down, and none of them were the rename itself.
The first was useImperativeHandle. If a component exposes a custom handle instead of a raw DOM node, the ref still needs wiring, and the codemod is right to be cautious there. The fix is the same idea as before, you just read ref from props now:
function VideoPlayer({ ref, src }) {
const videoRef = useRef(null);
useImperativeHandle(ref, () => ({
play: () => videoRef.current.play(),
pause: () => videoRef.current.pause(),
}));
return <video ref={videoRef} src={src} />;
}
The second was libraries. If a component library you depend on still ships forwardRef internally, that’s fine, it keeps working without changes. But you can’t assume every third-party component accepts ref as a prop yet, so check the library’s version and its docs before you rely on the new behavior. I hit this with a date-picker component I’d assumed I could attach a ref to directly. Its current release still wraps everything in forwardRef, which works fine, but a separate wrapper component in the same library doesn’t forward refs at all, on purpose, and reading the docs was the only way to tell which was which.
The third was the one I should have predicted. Several of my components spread props onto a DOM element with {...props}, and once ref is a prop, a careless spread can forward ref somewhere you didn’t intend or drop it entirely. One component destructured ref out, used it, then spread the rest, which is correct. Another spread everything blindly and quietly lost the ref. Tests didn’t catch it, because the ref only drove focus management. A real person tabbing through the form caught it. Now I’m deliberate about whether ref comes out of props before a spread.
So should you bother
If you’re on React 18 or earlier, none of this applies yet, and you shouldn’t upgrade a whole app just to delete some wrappers. The upgrade is worth doing for other reasons I’ve written about, and ref-as-prop is a small bonus you collect along the way rather than the headline.
If you’re already on React 19, convert your own components, and do it gradually. The payoff isn’t performance. It’s that there’s one less special rule in your codebase. New teammates don’t have to learn what forwardRef is or why it ever existed. ref reads like every other prop, because now it is one.
Here’s the concrete thing to do this week. Open the component you reach for most, the input or button wrapper everything else is built on, and migrate just that one. Run the codemod on it, read the diff, run your tests, then tab through the UI by hand to confirm the ref still does its job. One file. You’ll know within ten minutes whether your setup is ready for the rest, and you’ll have removed the wrapper you copy-pasted more than any other. If you want to see how I wire these pieces together on real projects, that’s over on my portfolio.