React 19 forwardRef Migration: ref as a Prop in TypeScript
Your components are full of forwardRef wrappers, displayName lines and ComponentPropsWithoutRef types, and React 19 says you no longer need them. The React 19 forwardRef change is small on paper: function components now receive ref as a regular prop. The work is at the edges. Props types that strip the ref, components that need the DOM node themselves, and asChild wrappers that hand the ref to a child all behave a little differently once the wrapper is gone. This tutorial migrates an input and a button, checked against React 19.3 and TypeScript 5.9, and shows the patterns we use in Wingo UI, whose source does not call forwardRef once.
What changed about forwardRef in React 19?
Function components get ref in their props, so the wrapper has nothing left to do. The React 19 release post (opens in a new tab) says new function components "will no longer need forwardRef" and that React will "deprecate and remove forwardRef" in future versions. The forwardRef reference (opens in a new tab) says the same: no longer necessary, deprecated in a future release.
So the React 19 forwardRef replacement is the ref prop itself. Four facts decide how you migrate:
- forwardRef still works and stays quiet. We searched the development builds of
reactandreact-dom19.3.0 for a deprecation message and found none. Old components keep running while you migrate. - Class components did not change. A ref passed to a class still points to the instance and never shows up in its props.
element.refis on its way out. React 19 still fills it, but reading it in development logs "Accessing element.ref was removed in React 19. ref is now a regular prop." Readelement.props.refinstead.- React 18 strips
reffrom props. A library that still lists React 18 as a peer dependency has to keepforwardRef. Radix does:@radix-ui/react-slot1.3.3 wraps itsSlotinReact.forwardRefand accepts React 16.8 through 19. Wingo UI requires React 19, so its components takerefas a plain prop.
How do you migrate a component from forwardRef to ref as a prop?
Delete the wrapper, type the props with ComponentProps, and let the ref travel in ...props. Here is the classic shadcn-style input before:
And after:
ComponentProps<"input"> already contains ref?: Ref<HTMLInputElement>, so the spread hands it to the <input> and <Input ref={inputRef} /> works without one ref-specific line. A named function gives React DevTools its name, so displayName goes too.
For a whole folder, start with the official codemod from reactjs/react-codemod (opens in a new tab), on a clean branch so you can read the diff:
shadcn/ui made the same move when it switched to React 19. The shadcn Tailwind v4 guide (opens in a new tab) lists the manual steps: replace React.forwardRef<...> with React.ComponentProps<...>, remove ref={ref}, add a data-slot attribute, convert to a named function and drop displayName. Their current Button types its props as React.ComponentProps<"button"> plus its variants and asChild. If your project still has the older shadcn forwardRef components with local edits, updating shadcn components without losing edits covers the merge.
To keep forwardRef from coming back, add a lint rule. As of October 2026, eslint-react's no-forward-ref (opens in a new tab) is in its recommended presets, and Biome's noReactForwardRef (opens in a new tab) (since Biome 2.2.5) is opt-in with an unsafe fix. Leave them off in packages that still support React 18.
How do you type ref as a prop in React 19 with TypeScript?
Most of the time you type nothing, because ComponentProps<"button"> includes ref. You add ref?: Ref<HTMLButtonElement> only when you write the props type yourself. The other type changes are deletions and renames:
The last row trips people up the day they upgrade @types/react. Ref callbacks may return a cleanup function now, so an arrow function that implicitly returns the node fails with Type 'HTMLDivElement | null' is not assignable to type 'void | (() => VoidOrUndefinedOnly)'. Add braces and it compiles.
When the props are your own, declare the ref like any other prop:
Generic components get their type parameter back
Generic components gain the most from ref as a prop in React 19. forwardRef returns a component that is not generic, so a typed list needed a cast to get its <T> back:
Now it is a plain generic function, and T is inferred at the call site with the ref still typed:
Wrapper layers pass the ref without extra code
In our Button the ref never appears by name. ButtonProps is Omit<ComponentProps<"button">, "color" | ...> plus our props. The component reads them through useDefaults("button", buttonProps), which layers app-wide defaults from UIProvider under the props passed in, and spreads the rest onto motion.button. With forwardRef the ref would arrive as a second argument, and every layer between the caller and the element would need its own way to carry it. As a prop, it is one more key.
How do you merge refs in React 19?
Merge the two refs in a memoized callback ref that returns a cleanup. You need this when the component uses the DOM node itself and the caller wants it too. Our Input is the real case. It needs the node to clear the text with a real input event, put focus back after clearing and hear form resets, and your code needs the same node to focus the field after a failed submit. This is the pattern from components/ui/input.tsx, trimmed to a search field:
Three details matter:
- Cleanups. When the node goes away, React 19 calls the cleanup a ref callback returned, instead of calling the callback again with
null. SoattachRefruns the caller's cleanup if they returned one and falls back toref(null)if they did not. - A stable callback.
useCallbackkeepssetRefthe same between renders. A new callback ref on every render makes React detach and reattach it each time. In a StrictMode test, an inline ref callback logged its cleanup and a fresh attach after a plain re-render, while the memoized one stayed attached. - A clear React can hear.
clearValuegoes through the native value setter and fires aninputevent, so the caller'sonChangeruns with an empty value, like after a keystroke. Assigninginput.value = ""empties the field without telling React or a form library.
Textarea merges refs the same way, so it can measure its own node and grow with its content.
Using it from the outside is a normal ref:
React Hook Form's register passes a ref the same way, so {...register("email")} reaches the <input> with no adapter. The React form components guide covers which fields need Controller instead.
$ npx wingo-ui@latest add inputWhen you expose methods instead of a node, useImperativeHandle(ref, ...) works exactly as before, with ref from props. Our Pro Schema Builder hands out validate() and getJsonSchema() that way; see pricing for what Pro includes.
How does ref as a prop work with asChild and Slot?
Spread the props, ref included, onto Slot, and put the ref on the child when you need the child's type. Radix Slot composes the ref it receives with the child's own ref. In React 19 it reads the child's ref from element.props.ref, so neither one is lost and no warning fires.
The trap is TypeScript. A button component types its ref for <button>, so passing a link ref to the wrapper fails:
Put the ref on the child. Slot merges it with its own, and the type is right:
Our Button renders asChild through motion.create(Slot), so two refs want the same node: motion's, for the press spring, and yours. Slot composes both. We mounted this Button in Chromium under StrictMode with a plain anchor as the child: the child's ref received the <a>, and the console stayed empty.
$ npx wingo-ui@latest add buttonTwo more things break asChild after a migration:
- A child component that drops the ref. Slot passes
ref,classNameand event handlers to the child, and a component that does not spread its props loses all three. Radix tooltips and popovers find their trigger through that ref, so the content has no element to position against. - A homemade Slot that reads
child.ref. It still works at runtime, but logs theelement.referror in development, and@types/react19 no longer declaresrefonReactElement, so TypeScript rejects it too. Read it from props, with a type argument because element props default tounknownin React 19:
What should you try next?
Install the two free components from this post and read how they handle refs in your own project:
Both land in components/ui as source you own, along with the field, spinner and helpers they import, and none of those files use forwardRef. For the other half of the stack they target, browse the Next.js and Tailwind CSS guides, starting with Tailwind v4 design tokens.
FAQ
Is forwardRef deprecated in React 19?
Not formally. The React docs say forwardRef is no longer necessary in React 19 and will be deprecated in a future release. As of React 19.3 it still works and logs no warning, but new function components should take ref as a prop.
How do I type the ref prop in React 19 with TypeScript?
When you wrap one element, use ComponentProps<'button'>, which already includes ref. When you write the props type yourself, add ref?: Ref<HTMLButtonElement>. Stop using ComponentPropsWithoutRef for wrappers, and replace ElementRef with ComponentRef.
Do I still need forwardRef if my library supports React 18?
Yes. React 18 removes ref from props before your component runs, so only forwardRef gives you the ref there. Keep it until React 18 leaves your peer dependencies; forwardRef components still work in React 19.
Does asChild still work without forwardRef?
Yes. Radix Slot reads the child's ref from its props in React 19 and composes it with the ref the Slot receives. Put the ref on the child when you want its real type, such as HTMLAnchorElement for a link rendered through a button component.
Is there a codemod to remove forwardRef?
Yes. The reactjs/react-codemod project ships remove-forward-ref: run npx codemod react/19/remove-forward-ref --target components/ui on a clean branch, then review the diff, especially generic components and props types that still use ComponentPropsWithoutRef.
Do class components get ref as a prop in React 19?
No. A ref passed to a class component still points to the component instance and is not passed in its props.
- React 19
- TypeScript
- forwardRef
- shadcn/ui
- Refs