Skip to main content

React 19 Migration: How to Fix TypeScript Errors and Replace forwardRef

 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:

  1. Remove the forwardRef wrapper.
  2. Update the Props interface to explicitly accept a ref (or extend standard element props).
  3. Destructure ref from 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.

  1. Clarity: It signals to other developers that this component consumes a ref.
  2. Type Safety: It avoids passing the ref meant 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.

Popular posts from this blog

Restricting Jetpack Compose TextField to Numeric Input Only

Jetpack Compose has revolutionized Android development with its declarative approach, enabling developers to build modern, responsive UIs more efficiently. Among the many components provided by Compose, TextField is a critical building block for user input. However, ensuring that a TextField accepts only numeric input can pose challenges, especially when considering edge cases like empty fields, invalid characters, or localization nuances. In this blog post, we'll explore how to restrict a Jetpack Compose TextField to numeric input only, discussing both basic and advanced implementations. Why Restricting Input Matters Restricting user input to numeric values is a common requirement in apps dealing with forms, payment entries, age verifications, or any data where only numbers are valid. Properly validating input at the UI level enhances user experience, reduces backend validation overhead, and minimizes errors during data processing. Compose provides the flexibility to implement ...

jetpack compose - TextField remove underline

Compose TextField Remove Underline The TextField is the text input widget of android jetpack compose library. TextField is an equivalent widget of the android view system’s EditText widget. TextField is used to enter and modify text. The following jetpack compose tutorial will demonstrate to us how we can remove (actually hide) the underline from a TextField widget in an android application. We have to apply a simple trick to remove (hide) the underline from the TextField. The TextField constructor’s ‘colors’ argument allows us to set or change colors for TextField’s various components such as text color, cursor color, label color, error color, background color, focused and unfocused indicator color, etc. Jetpack developers can pass a TextFieldDefaults.textFieldColors() function with arguments value for the TextField ‘colors’ argument. There are many arguments for this ‘TextFieldDefaults.textFieldColors()’function such as textColor, disabledTextColor, backgroundColor, cursorC...

jetpack compose - Image clickable

Compose Image Clickable The Image widget allows android developers to display an image object to the app user interface using the jetpack compose library. Android app developers can show image objects to the Image widget from various sources such as painter resources, vector resources, bitmap, etc. Image is a very essential component of the jetpack compose library. Android app developers can change many properties of an Image widget by its modifiers such as size, shape, etc. We also can specify the Image object scaling algorithm, content description, etc. But how can we set a click event to an Image widget in a jetpack compose application? There is no built-in property/parameter/argument to set up an onClick event directly to the Image widget. This android application development tutorial will demonstrate to us how we can add a click event to the Image widget and make it clickable. Click event of a widget allow app users to execute a task such as showing a toast message by cli...