The release of React 19 marks a significant architectural simplification in how references interact with the reconciliation process. For years, ref was treated as a "magic" prop, stripped from the props object before it reached your component, necessitating the usage of forwardRef to tunnel it back through.
In React 19, ref is now a standard prop. While this reduces API surface area, it introduces breaking changes for legacy patterns, specifically triggering TypeScript mismatches and runtime errors when accessing refs on React Elements directly.
The Root Cause: Why forwardRef is Dead
To understand the error, you must understand the structural change in ReactElement.
In React 18 and older, when you wrote <Child ref={myRef} />, the React compiler generated an element where ref was a distinct property on the fiber node, separate from props. Accessing props.ref inside the component returned undefined. To pass a ref to a child, you had to wrap the component in forwardRef, which created a Higher-Order Component specifically designed to inject the ref as a second argument.
In React 19, this special handling is removed for function components.
- Ref as Prop: The
refis passed directly in thepropsobject, just likeclassNameorid. - Element Access: Because
refis moved to props, accessing the propertyelement.refon a JSX element instance (common in HOCs, test utilities, or cloneElement patterns) now throws a warning or error, as the getter has been deprecated. The ref now lives atelement.props.ref.
The Fix: Migrating Components and Types
Below are the technically rigorous steps to migrate a component from the forwardRef pattern to the React 19 standard, including the necessary TypeScript adjustments.
1. The Component Migration
The removal of forwardRef allows us to write components as pure functions.
Legacy Code (React 18):
import { forwardRef, type ComponentProps } from 'react';
interface TextInputProps extends ComponentProps<'input'> {
label: string;
}
// ❌ OLD: Requires forwardRef wrapper and separate ref argument
export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
({ label, ...props }, ref) => {
return (
<div className="flex flex-col gap-2">
<label>{label}</label>
<input
ref={ref}
className="border p-2 rounded"
{...props}
/>
</div>
);
}
);
TextInput.displayName = 'TextInput';
Modern Code (React 19):
In React 19, we destructure ref directly from props. Note that we do not need to wrap the export.
import { type ComponentProps } from 'react';
// ✅ NEW: Extend ComponentProps to automatically include the 'ref' type for intrinsic elements
interface TextInputProps extends ComponentProps<'input'> {
label: string;
}
export const TextInput = ({ label, ref, ...props }: TextInputProps) => {
return (
<div className="flex flex-col gap-2">
<label>{label}</label>
<input
ref={ref}
className="border p-2 rounded"
{...props}
/>
</div>
);
};
2. Fixing "Property 'ref' does not exist on type..."
A common point of friction during this migration is TypeScript interfaces. If you are not extending ComponentProps (e.g., you are defining a strictly controlled interface), you must explicitely type the ref.
The Interface Pattern:
import { type Ref, type InputHTMLAttributes } from 'react';
interface CustomButtonProps extends InputHTMLAttributes<HTMLButtonElement> {
variant: 'primary' | 'secondary';
// explicitly define ref if not using ComponentProps
ref?: Ref<HTMLButtonElement>;
}
export const CustomButton = ({ variant, ref, ...props }: CustomButtonProps) => {
return (
<button
ref={ref}
data-variant={variant}
{...props}
/>
);
}
3. Resolving "Accessing element.ref is no longer supported"
This runtime error frequently appears in Higher-Order Components, Test Utilities, or Layout Managers that inspect children to clone them or extract refs.
If your codebase iterates over children to access refs, you are likely accessing the deprecated getter on the React Element.
Legacy Utility (Breaks in v19):
import { Children, isValidElement, type ReactNode } from 'react';
export function logChildRefs(children: ReactNode) {
Children.forEach(children, (child) => {
if (isValidElement(child)) {
// ❌ RUNTIME ERROR in v19: Accessing ref directly on the element
console.log('Found ref:', child.ref);
}
});
}
Refactored Utility (v19 Compatible):
You must attempt to access props.ref. However, because ref was not in props in v18, a robust migration utility should check both locations if you are maintaining a library that supports multiple React versions, or strictly props.ref for v19-only codebases.
import { Children, isValidElement, type ReactNode, type ReactElement } from 'react';
export function logChildRefs(children: ReactNode) {
Children.forEach(children, (child) => {
if (isValidElement(child)) {
// ✅ FIX: Access ref via props.
// We cast to any or a specific type because TS might not know
// the element has a ref prop depending on your strictness settings.
const elementProps = child.props as { ref?: unknown };
console.log('Found ref:', elementProps.ref);
}
});
}
Technical Explanation: Why this works
By removing forwardRef, React aligns the mental model of Refs with Props.
- Instantiation Performance:
forwardRefcreated an extra layer in the component tree solely for prop tunneling. Removing it flattens the React Fiber tree, resulting in marginally reduced memory usage and faster reconciliation during deep updates. - Prop Drilling: Since
refis now a prop, you can drill it just like any other value. If you have aFormFieldwrapper that needs to pass a ref down to anInput, you no longer needforwardRefon the intermediateFormField. You simply passref={props.ref}down the chain.
Codemod Strategy
For large codebases, you cannot manually rewrite every component. Use jscodeshift to automate the stripping of forwardRef.
If you are using the React 19 codemods provided by the React team:
npx react-codemod@latest react-19/remove-forward-ref <path/to/source>
This script detects forwardRef usage, unwraps the inner function, adds ref to the props destructuring, and cleans up the imports.
Conclusion
The deprecation of forwardRef in React 19 is a "quality of life" improvement that eliminates boilerplate. While the migration requires touching almost every component that exposes a DOM node, the result is cleaner, standard JavaScript function signatures and better TypeScript inference. Ensure you update your internal HOCs to stop reading element.ref and switch to element.props.ref to avoid runtime crashes.