The removal of forwardRef is one of the most significant developer experience improvements in React 19. For years, ref was treated as a special, magic attribute that required a Higher-Order Component (HOC) wrapper to tunnel through to a child component.
In React 19, ref is just a prop.
However, upgrading isn't seamless. If you simply strip the forwardRef wrapper, TypeScript will likely throw errors such as Property 'ref' does not exist on type 'Props' or fail to infer the correct HTML element type.
This guide details the root cause of these errors and provides the standard pattern for typing and implementing refs in React 19.
The Root Cause: Why code breaks
In React 18 and earlier, ref and key were not actual props. When JSX was compiled, React stripped these attributes from the props object before passing the object to your component function. To access the ref, you had to use React.forwardRef, which received the ref as a separate, second argument.
React 18 Pattern (Legacy):
// The ref is NOT in props; it is a second argument
const Input = forwardRef((props, ref) => { ... });
In React 19, the runtime no longer strips ref. It is delivered inside the props object, just like className or id. Consequently, forwardRef is unnecessary overhead.
The TypeScript friction arises because your existing component interfaces likely do not define ref as a valid property, or your code attempts to access ref as a second argument, which no longer exists in standard functional components.
The Fix: Migrating to Ref as a Prop
To fix this, we need to:
- Remove the
forwardRefwrapper. - Update the Props interface to explicitly accept a
ref(or extend standard element props). - Destructure
reffrom the props object.
1. The Legacy Code (Before)
Here is a typical React 18 component wrapping a native input. It uses forwardRef and complex generics.
import { forwardRef, InputHTMLAttributes } from 'react';
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
label: string;
error?: string;
}
// ❌ DEPRECATED PATTERN
export const TextInput = forwardRef<HTMLInputElement, InputProps>(
({ label, error, ...props }, ref) => {
return (
<div className="input-wrapper">
<label>{label}</label>
<input ref={ref} {...props} />
{error && <span className="error">{error}</span>}
</div>
);
}
);
TextInput.displayName = 'TextInput';
2. The React 19 Solution (After)
In React 19, we treat the component as a standard function. We do not need to import RefObject or forwardRef.
However, for TypeScript to allow <TextInput ref={...} />, the ref must be recognized in the types. The cleanest way to achieve this is using React.ComponentPropsWithRef or explicitly typing the prop.
import { type ComponentPropsWithRef } from 'react';
// 1. Extend ComponentPropsWithRef to automatically include the correct 'ref' type
// for an 'input' element.
interface InputProps extends ComponentPropsWithRef<'input'> {
label: string;
error?: string;
}
// 2. No forwardRef wrapper. 'ref' is destructured from props.
export const TextInput = ({ label, error, ref, ...props }: InputProps) => {
return (
<div className="input-wrapper">
<label>{label}</label>
{/* 3. Pass the ref directly to the element */}
<input ref={ref} {...props} />
{error && <span className="error">{error}</span>}
</div>
);
};
3. Handling Polymorphic Refs
If you are building a generic component where the ref type is not strictly an HTML element (for example, a custom component exposing an imperative handle), you should explicitly type the ref prop using React.Ref.
import { type Ref, useImperativeHandle, useRef } from 'react';
// Define the shape of the imperative handle
export interface VideoPlayerHandle {
play: () => void;
pause: () => void;
}
interface VideoPlayerProps {
src: string;
// Explicitly type the ref prop
ref?: Ref<VideoPlayerHandle>;
}
export const VideoPlayer = ({ src, ref }: VideoPlayerProps) => {
const internalVideoRef = useRef<HTMLVideoElement>(null);
// useImperativeHandle works exactly the same,
// but it consumes the 'ref' passed in via props.
useImperativeHandle(ref, () => ({
play: () => internalVideoRef.current?.play(),
pause: () => internalVideoRef.current?.pause(),
}));
return <video src={src} ref={internalVideoRef} />;
};
Technical Nuances
Strict Prop Destructuring
You might be tempted to pass props straight through like this: <input {...props} />.
While this works at runtime, destructuring ref is best practice.
- Clarity: It signals to other developers that this component consumes a ref.
- Type Safety: It avoids passing the
refmeant for the wrapper (the React Component) recursively down to the child (the DOM node) if you are manipulating the ref before passing it.
TypeScript Interface Changes
If you previously used React.FC (Functional Component), you might notice that ref is not included in the default generic props.
While the React team is updating global type definitions (like @types/react) to include ref in JSX elements automatically, explicit interface definitions remain the most robust way to ensure type safety in a large codebase.
Using extends ComponentPropsWithRef<'element'> is the preferred method over InputHTMLAttributes because ComponentPropsWithRef includes the ref property definition, whereas InputHTMLAttributes historically only included standard HTML attributes, causing the "Property ref does not exist" error.
Conclusion
The shift away from forwardRef simplifies the React mental model. We no longer treat refs as "magic" side-channel data; they are simply props that point to instances. By switching your type definitions to ComponentPropsWithRef and destructuring ref from your props object, you eliminate TypeScript errors and align your codebase with modern React 19 standards.