cd /news/developer-tools/react-20-ref-as-a-prop-migrating-awa… · home topics developer-tools article
[ARTICLE · art-104089] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

React 20 `ref` as a Prop: Migrating Away From `forwardRef` Across a Large Component Library

React 20 treats ref as a standard prop, eliminating the need for forwardRef wrappers. Component library teams face a migration path that requires careful planning across versioning, TypeScript definitions, and test coverage to avoid breaking consuming applications.

read13 min views1 publishedAug 20, 2026

ref

as a Prop: Migrating Away From forwardRef

Across a Large Component Library

This article was written with the assistance of AI, under human supervision and review.

Most React component library maintenance debt stems from a single historical artifact: forwardRef

. The pattern emerged because refs were special-cased in React's original architecture—passing them required wrapping every component that needed to expose a DOM handle. Component library teams spent years adding forwardRef

wrappers to hundreds of components, maintaining parallel prop interfaces, and explaining to developers why some components accepted refs while others did not.

React 20 eliminates this complexity by treating ref

as a standard prop. The wrapper disappears. The special case vanishes. Teams maintaining component libraries face a straightforward migration path, but the execution requires deliberate planning across versioning, TypeScript definitions, and test coverage.

React 20's approach removes the wrapper entirely. The ref

prop flows through component props like className

or onClick

. TypeScript inference improves because the prop interface becomes a single object instead of a split between props and ref parameters.

This distinction is critical. Component libraries shipping to thousands of projects must execute this migration without breaking consuming applications. The path forward balances backward compatibility, versioning hygiene, and TypeScript correctness.

ref

as a standard prop, eliminating the need for forwardRef

wrappers in all component definitions.ref

as a prop field and removing forwardRef

function wrappers from component exports.React's original architecture treated refs as a special case because the reconciler needed direct control over DOM element references during commit phases. The forwardRef

API emerged as a workaround—a way to thread refs through component boundaries when the props object deliberately excluded them. This created a bifurcation in how developers thought about component APIs: regular props went through the props object, but refs required a separate code path.

The consequence was immediate and pervasive. Every component library that exposed DOM elements to consumers needed forwardRef

wrappers. A simple button component became a higher-order function. TypeScript definitions split into two parts: the props interface and the ref type parameter. Documentation had to explain why some components accepted refs while others did not, even when both rendered DOM elements.

React 20 resolves this by integrating ref handling directly into the reconciler's props diffing algorithm. When the reconciler processes a component's props, it now handles ref

assignments the same way it handles event handlers or style objects. The special case disappears from the API surface.

The implication here is that component library authors no longer maintain two parallel APIs for the same component. A button that accepts onClick

can accept ref

through the same props object. TypeScript inference works uniformly across all props. The cognitive overhead of explaining ref forwarding to new team members vanishes.

This matters because component libraries often contain hundreds of components. Each forwardRef

wrapper represents a maintenance point—a place where TypeScript generics might drift, where documentation must stay synchronized, where automated refactoring tools struggle. Eliminating these wrappers reduces the surface area for bugs and simplifies onboarding for contributors.

The first step in any large-scale migration is establishing which components require changes. Not every component in a library uses forwardRef

, and not every component that renders a DOM element needs to expose a ref. The migration targets components where external consumers expect to attach refs—typically leaf components that wrap native HTML elements or third-party DOM-producing libraries.

Start by scanning the codebase for forwardRef

imports. A simple grep or AST-based search identifies these components immediately. Cross-reference this list against the library's public API documentation. Any component documented as "ref-capable" must be updated, even if the current implementation does not use forwardRef

.

The second filter is usage data. If the library has telemetry or download statistics, prioritize components that appear in the most consuming projects. A button component with 50,000 weekly downloads demands migration before an obscure utility component with 200. This prioritization lets teams ship incremental releases, spreading the migration risk across multiple versions.

Components that render other components from the same library typically do not need changes. If a Card

component renders a Button

, and both are in the same library, the Card

does not need to forward refs—the consuming application attaches refs directly to the Button

. This reduces the migration scope significantly in libraries with deep component hierarchies.

The edge cases appear in higher-order components and render prop patterns. A HOC that wraps an arbitrary component must decide whether to expose the wrapped component's ref. In React 19, this required forwardRef

at the HOC level. In React 20, the HOC accepts ref

as a prop and passes it through manually. The pattern changes, but the core logic remains.

The mechanical transformation from forwardRef

to a standard prop follows a consistent pattern. Here is a typical button component in React 19:

import { forwardRef, ButtonHTMLAttributes } from 'react';

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  variant: 'primary' | 'secondary';
}

const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ variant, children, ...props }, ref) => {
    const className = variant === 'primary' ? 'btn-primary' : 'btn-secondary';
    return (
      <button ref={ref} className={className} {...props}>
        {children}
      </button>
    );
  }
);

Button.displayName = 'Button';

export default Button;

The React 20 version eliminates the wrapper function and accepts ref

as a standard prop:

import { Ref, ButtonHTMLAttributes } from 'react';

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  variant: 'primary' | 'secondary';
  ref?: Ref<HTMLButtonElement>;
}

function Button({ variant, children, ref, ...props }: ButtonProps) {
  const className = variant === 'primary' ? 'btn-primary' : 'btn-secondary';
  return (
    <button ref={ref} className={className} {...props}>
      {children}
    </button>
  );
}

export default Button;

The changes are minimal but load-bearing. The forwardRef

wrapper disappears. The ref

prop moves into the ButtonProps

interface as an optional field. The function signature becomes a standard function component instead of a callback within forwardRef

. The displayName

assignment becomes unnecessary because the function name provides it directly.

This pattern scales across component complexity. A more complex component with multiple refs requires explicit prop names, but the structure remains identical. Consider a split-pane component that exposes refs to both panes:

import { Ref } from 'react';

interface SplitPaneProps {
  leftRef?: Ref<HTMLDivElement>;
  rightRef?: Ref<HTMLDivElement>;
  leftContent: React.ReactNode;
  rightContent: React.ReactNode;
}

function SplitPane({ leftRef, rightRef, leftContent, rightContent }: SplitPaneProps) {
  return (
    <div className="split-container">
      <div ref={leftRef} className="split-left">
        {leftContent}
      </div>
      <div ref={rightRef} className="split-right">
        {rightContent}
      </div>
    </div>
  );
}

export default SplitPane;

No forwardRef

wrapper appears. The refs flow through props like any other value. The consuming code remains unchanged—developers still pass leftRef={myRef}

when rendering the component.

The failure mode here is subtle but expensive. Teams that forget to add ref

to the TypeScript interface will ship components that accept refs at runtime but fail TypeScript compilation in consuming projects. The migration must include interface updates alongside function signature changes, and automated tests must verify both paths.

TypeScript definitions require deliberate updates during migration. The forwardRef

API used a second type parameter for the ref type, separated from the props interface. React 20 collapses this into a single interface, but the type definitions must match the runtime behavior exactly.

Start by importing the Ref

type from React. This type represents all valid ref values: callback refs, object refs from useRef

, and null. Add it to the props interface as an optional field:

import { Ref } from 'react';

interface InputProps {
  label: string;
  placeholder?: string;
  ref?: Ref<HTMLInputElement>;
}

The optional marker is critical. Most consuming code does not attach refs to every component instance. Making ref

required would break existing usage patterns and force consumers to pass ref={null}

explicitly—a poor developer experience.

Generic components introduce additional complexity. A List<T>

component that renders items of type T

might need a ref to the container element. The generic type parameter must not conflict with the ref type:

import { Ref } from 'react';

interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  ref?: Ref<HTMLUListElement>;
}

function List<T>({ items, renderItem, ref }: ListProps<T>) {
  return (
    <ul ref={ref}>
      {items.map((item, index) => (
        <li key={index}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

The T

parameter applies to the items array, while Ref<HTMLUListElement>

applies to the container. TypeScript infers both independently. This pattern works because React 20 does not require special handling for refs in generic components—they are just props.

Higher-order components that wrap arbitrary components face a different challenge. The HOC must preserve the wrapped component's ref type while adding its own props. This requires conditional types:

import { ComponentType, Ref, ComponentPropsWithoutRef } from 'react';

function withLogger<P extends object>(
  Component: ComponentType<P>
): ComponentType<P & { ref?: Ref<any> }> {
  return function LoggedComponent(props: P & { ref?: Ref<any> }) {
    console.log('Rendering with props:', props);
    return <Component {...props} />;
  };
}

This approach is fragile and error-prone in large codebases. The better pattern is to avoid ref forwarding in HOCs entirely. If consumers need a ref to the wrapped component, they should render it directly instead of wrapping it in a HOC. This aligns with React's composition philosophy and reduces maintenance burden.

The distinction here matters for libraries shipping to diverse TypeScript configurations. Some consuming projects enable strict null checks; others do not. The ref

type must work correctly in both modes, which means using Ref<T>

instead of custom union types or optional chaining assumptions.

Migrating from forwardRef

to ref-as-prop represents a breaking change for any component library. The runtime behavior remains compatible—components still accept refs—but the TypeScript definitions change shape. Consuming projects that import component types directly will see compilation errors until they update.

The versioning strategy must follow semantic versioning strictly. Increment the major version number when shipping the migration. Document the breaking changes in the changelog with specific examples of old and new usage patterns. Provide a migration guide that shows the diff for common component types.

Some teams attempt to maintain backward compatibility by shipping both forwardRef

and ref-as-prop versions in parallel. This strategy creates a maintenance nightmare. The codebase doubles in size for the duration of the compatibility window. Test coverage must verify both code paths. Documentation must explain when to use each variant.

A cleaner approach is a hard cutover with a deprecation period. Ship the new major version with ref-as-prop exclusively. Mark the old version as deprecated in the package registry. Provide a compatibility shim for teams that cannot upgrade immediately:

// compatibility-shim.ts
import { forwardRef, ComponentType } from 'react';

export function createForwardRefShim<P extends object>(
  Component: ComponentType<P>
) {
  return forwardRef<any, Omit<P, 'ref'>>((props, ref) => {
    return <Component {...(props as P)} ref={ref} />;
  });
}

This shim wraps the new ref-as-prop component in a forwardRef

wrapper, providing the old API surface for consumers who have not migrated yet. The shim ships as a separate export, not as the default behavior, so teams opt into compatibility explicitly.

The failure mode here is releasing the migration without adequate communication. Developers upgrading to the new major version encounter TypeScript errors with no clear explanation. The changelog must include a "Migration Guide" section with before-and-after code examples for every common component type in the library.

Related patterns for handling breaking changes in production React applications appear in React 19 concurrent rendering production patterns and React error boundaries production patterns.

Test coverage for ref forwarding requires verifying both runtime behavior and TypeScript compilation. The runtime tests confirm that refs attach to the correct DOM elements. The type tests ensure that consuming code compiles without errors when passing refs.

Start with a basic runtime test using a testing library like Jest and React Testing Library:

import { render } from '@testing-library/react';
import { useRef, useEffect } from 'react';
import Button from './Button';

test('Button forwards ref to underlying button element', () => {
  let capturedRef: HTMLButtonElement | null = null;

  function TestComponent() {
    const buttonRef = useRef<HTMLButtonElement>(null);

    useEffect(() => {
      capturedRef = buttonRef.current;
    }, []);

    return <Button ref={buttonRef} variant="primary">Click</Button>;
  }

  render(<TestComponent />);

  expect(capturedRef).toBeInstanceOf(HTMLButtonElement);
  expect(capturedRef?.tagName).toBe('BUTTON');
});

This test renders the component, attaches a ref, and verifies that the ref points to the correct DOM element type. The test catches regressions where the ref assignment gets dropped during refactoring.

Type-level tests require a different approach. Use TypeScript's expectType

utility from libraries like tsd

or expect-type

:

import { expectType } from 'tsd';
import { useRef } from 'react';
import Button from './Button';

const buttonRef = useRef<HTMLButtonElement>(null);

// Should compile without errors
expectType<JSX.Element>(<Button ref={buttonRef} variant="primary">Click</Button>);

// Should reject invalid ref types
// @ts-expect-error
expectType<JSX.Element>(<Button ref={useRef<HTMLDivElement>(null)} variant="primary">Click</Button>);

These type tests run during CI and fail the build if component interfaces drift. The tests verify that ref

accepts the correct element type and rejects incompatible types.

For component libraries with hundreds of components, generate ref forwarding tests automatically. Write a script that scans the codebase for exported components, generates a test file for each, and runs the suite during CI. This approach ensures comprehensive coverage without manual test authoring.

The edge case appears in components that conditionally render different elements based on props. A component that renders either a button

or an a

element depending on an href

prop must type the ref union correctly:

import { Ref } from 'react';

interface ButtonLinkProps {
  href?: string;
  children: React.ReactNode;
  ref?: Ref<HTMLButtonElement | HTMLAnchorElement>;
}

function ButtonLink({ href, children, ref }: ButtonLinkProps) {
  if (href) {
    return <a ref={ref as Ref<HTMLAnchorElement>} href={href}>{children}</a>;
  }
  return <button ref={ref as Ref<HTMLButtonElement>}>{children}</button>;
}

Testing these components requires separate test cases for each rendering path, verifying that the ref attaches to the correct element type in each scenario.

Existing forwardRef

usage continues to work in React 20—the API remains supported for backward compatibility. However, new code should adopt ref-as-prop to avoid the wrapper overhead and simplify TypeScript definitions.

Yes, but maintaining both versions doubles the maintenance burden and test surface area. A cleaner approach is to ship ref-as-prop exclusively in a new major version and provide a compatibility shim for teams that need the old API temporarily.

HOCs accept ref

as a standard prop and pass it through to the wrapped component. The HOC's props interface must include ref

with the appropriate element type, and the component must forward it explicitly in the JSX.

No special handling is required. Generic components accept ref

as a prop with the appropriate element type, and TypeScript infers both the generic type parameter and the ref type independently without conflicts.

Runtime tests should render the component with a ref, capture the ref value in a useEffect

, and assert that it points to the correct DOM element. Type-level tests using expectType

should verify that the component accepts valid ref types and rejects invalid ones.

The migration from forwardRef

to ref-as-prop eliminates a historical artifact that added complexity without delivering proportional value. Component libraries that complete this migration reduce their maintenance surface, improve TypeScript inference, and simplify onboarding for contributors who no longer need to understand why refs require special handling.

The execution requires discipline: semantic versioning, comprehensive testing, clear migration documentation, and a willingness to treat the change as the breaking change it is. Teams that rush the migration without adequate communication will see support requests spike as consumers encounter unexpected TypeScript errors.

That covers the essential patterns for migrating large component libraries to React 20's ref-as-prop system. Apply these in production and the difference will be immediate—fewer wrapper functions, cleaner type definitions, and a codebase that aligns with React's evolving composition model.

── more in #developer-tools 4 stories · sorted by recency
── more on @react 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/react-20-ref-as-a-pr…] indexed:0 read:13min 2026-08-20 ·