{"slug": "react-20-ref-as-a-prop-migrating-away-from-forwardref-across-a-large-component", "title": "React 20 `ref` as a Prop: Migrating Away From `forwardRef` Across a Large Component Library", "summary": "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.", "body_md": "`ref`\n\nas a Prop: Migrating Away From `forwardRef`\n\nAcross a Large Component Library\n\nThis article was written with the assistance of AI, under human supervision and review.\n\nMost React component library maintenance debt stems from a single historical artifact: `forwardRef`\n\n. 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`\n\nwrappers to hundreds of components, maintaining parallel prop interfaces, and explaining to developers why some components accepted refs while others did not.\n\nReact 20 eliminates this complexity by treating `ref`\n\nas 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.\n\nReact 20's approach removes the wrapper entirely. The `ref`\n\nprop flows through component props like `className`\n\nor `onClick`\n\n. TypeScript inference improves because the prop interface becomes a single object instead of a split between props and ref parameters.\n\nThis 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.\n\n`ref`\n\nas a standard prop, eliminating the need for `forwardRef`\n\nwrappers in all component definitions.`ref`\n\nas a prop field and removing `forwardRef`\n\nfunction 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`\n\nAPI 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.\n\nThe consequence was immediate and pervasive. Every component library that exposed DOM elements to consumers needed `forwardRef`\n\nwrappers. 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.\n\nReact 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`\n\nassignments the same way it handles event handlers or style objects. The special case disappears from the API surface.\n\nThe implication here is that component library authors no longer maintain two parallel APIs for the same component. A button that accepts `onClick`\n\ncan accept `ref`\n\nthrough the same props object. TypeScript inference works uniformly across all props. The cognitive overhead of explaining ref forwarding to new team members vanishes.\n\nThis matters because component libraries often contain hundreds of components. Each `forwardRef`\n\nwrapper 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.\n\nThe first step in any large-scale migration is establishing which components require changes. Not every component in a library uses `forwardRef`\n\n, 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.\n\nStart by scanning the codebase for `forwardRef`\n\nimports. 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`\n\n.\n\nThe 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.\n\nComponents that render other components from the same library typically do not need changes. If a `Card`\n\ncomponent renders a `Button`\n\n, and both are in the same library, the `Card`\n\ndoes not need to forward refs—the consuming application attaches refs directly to the `Button`\n\n. This reduces the migration scope significantly in libraries with deep component hierarchies.\n\nThe 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`\n\nat the HOC level. In React 20, the HOC accepts `ref`\n\nas a prop and passes it through manually. The pattern changes, but the core logic remains.\n\nThe mechanical transformation from `forwardRef`\n\nto a standard prop follows a consistent pattern. Here is a typical button component in React 19:\n\n``` js\nimport { forwardRef, ButtonHTMLAttributes } from 'react';\n\ninterface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n  variant: 'primary' | 'secondary';\n}\n\nconst Button = forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ variant, children, ...props }, ref) => {\n    const className = variant === 'primary' ? 'btn-primary' : 'btn-secondary';\n    return (\n      <button ref={ref} className={className} {...props}>\n        {children}\n      </button>\n    );\n  }\n);\n\nButton.displayName = 'Button';\n\nexport default Button;\n```\n\nThe React 20 version eliminates the wrapper function and accepts `ref`\n\nas a standard prop:\n\n``` js\nimport { Ref, ButtonHTMLAttributes } from 'react';\n\ninterface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n  variant: 'primary' | 'secondary';\n  ref?: Ref<HTMLButtonElement>;\n}\n\nfunction Button({ variant, children, ref, ...props }: ButtonProps) {\n  const className = variant === 'primary' ? 'btn-primary' : 'btn-secondary';\n  return (\n    <button ref={ref} className={className} {...props}>\n      {children}\n    </button>\n  );\n}\n\nexport default Button;\n```\n\nThe changes are minimal but load-bearing. The `forwardRef`\n\nwrapper disappears. The `ref`\n\nprop moves into the `ButtonProps`\n\ninterface as an optional field. The function signature becomes a standard function component instead of a callback within `forwardRef`\n\n. The `displayName`\n\nassignment becomes unnecessary because the function name provides it directly.\n\nThis 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:\n\n``` js\nimport { Ref } from 'react';\n\ninterface SplitPaneProps {\n  leftRef?: Ref<HTMLDivElement>;\n  rightRef?: Ref<HTMLDivElement>;\n  leftContent: React.ReactNode;\n  rightContent: React.ReactNode;\n}\n\nfunction SplitPane({ leftRef, rightRef, leftContent, rightContent }: SplitPaneProps) {\n  return (\n    <div className=\"split-container\">\n      <div ref={leftRef} className=\"split-left\">\n        {leftContent}\n      </div>\n      <div ref={rightRef} className=\"split-right\">\n        {rightContent}\n      </div>\n    </div>\n  );\n}\n\nexport default SplitPane;\n```\n\nNo `forwardRef`\n\nwrapper appears. The refs flow through props like any other value. The consuming code remains unchanged—developers still pass `leftRef={myRef}`\n\nwhen rendering the component.\n\nThe failure mode here is subtle but expensive. Teams that forget to add `ref`\n\nto 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.\n\nTypeScript definitions require deliberate updates during migration. The `forwardRef`\n\nAPI 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.\n\nStart by importing the `Ref`\n\ntype from React. This type represents all valid ref values: callback refs, object refs from `useRef`\n\n, and null. Add it to the props interface as an optional field:\n\n``` js\nimport { Ref } from 'react';\n\ninterface InputProps {\n  label: string;\n  placeholder?: string;\n  ref?: Ref<HTMLInputElement>;\n}\n```\n\nThe optional marker is critical. Most consuming code does not attach refs to every component instance. Making `ref`\n\nrequired would break existing usage patterns and force consumers to pass `ref={null}`\n\nexplicitly—a poor developer experience.\n\nGeneric components introduce additional complexity. A `List<T>`\n\ncomponent that renders items of type `T`\n\nmight need a ref to the container element. The generic type parameter must not conflict with the ref type:\n\n``` js\nimport { Ref } from 'react';\n\ninterface ListProps<T> {\n  items: T[];\n  renderItem: (item: T) => React.ReactNode;\n  ref?: Ref<HTMLUListElement>;\n}\n\nfunction List<T>({ items, renderItem, ref }: ListProps<T>) {\n  return (\n    <ul ref={ref}>\n      {items.map((item, index) => (\n        <li key={index}>{renderItem(item)}</li>\n      ))}\n    </ul>\n  );\n}\n```\n\nThe `T`\n\nparameter applies to the items array, while `Ref<HTMLUListElement>`\n\napplies 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.\n\nHigher-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:\n\n``` js\nimport { ComponentType, Ref, ComponentPropsWithoutRef } from 'react';\n\nfunction withLogger<P extends object>(\n  Component: ComponentType<P>\n): ComponentType<P & { ref?: Ref<any> }> {\n  return function LoggedComponent(props: P & { ref?: Ref<any> }) {\n    console.log('Rendering with props:', props);\n    return <Component {...props} />;\n  };\n}\n```\n\nThis 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.\n\nThe distinction here matters for libraries shipping to diverse TypeScript configurations. Some consuming projects enable strict null checks; others do not. The `ref`\n\ntype must work correctly in both modes, which means using `Ref<T>`\n\ninstead of custom union types or optional chaining assumptions.\n\nMigrating from `forwardRef`\n\nto 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.\n\nThe 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.\n\nSome teams attempt to maintain backward compatibility by shipping both `forwardRef`\n\nand 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.\n\nA 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:\n\n``` js\n// compatibility-shim.ts\nimport { forwardRef, ComponentType } from 'react';\n\nexport function createForwardRefShim<P extends object>(\n  Component: ComponentType<P>\n) {\n  return forwardRef<any, Omit<P, 'ref'>>((props, ref) => {\n    return <Component {...(props as P)} ref={ref} />;\n  });\n}\n```\n\nThis shim wraps the new ref-as-prop component in a `forwardRef`\n\nwrapper, 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.\n\nThe 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.\n\nRelated patterns for handling breaking changes in production React applications appear in [React 19 concurrent rendering production patterns](https://jsmanifest.com/react-19-concurrent-rendering-production-patterns) and [React error boundaries production patterns](https://jsmanifest.com/react-error-boundaries-production-patterns).\n\nTest 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.\n\nStart with a basic runtime test using a testing library like Jest and React Testing Library:\n\n``` js\nimport { render } from '@testing-library/react';\nimport { useRef, useEffect } from 'react';\nimport Button from './Button';\n\ntest('Button forwards ref to underlying button element', () => {\n  let capturedRef: HTMLButtonElement | null = null;\n\n  function TestComponent() {\n    const buttonRef = useRef<HTMLButtonElement>(null);\n\n    useEffect(() => {\n      capturedRef = buttonRef.current;\n    }, []);\n\n    return <Button ref={buttonRef} variant=\"primary\">Click</Button>;\n  }\n\n  render(<TestComponent />);\n\n  expect(capturedRef).toBeInstanceOf(HTMLButtonElement);\n  expect(capturedRef?.tagName).toBe('BUTTON');\n});\n```\n\nThis 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.\n\nType-level tests require a different approach. Use TypeScript's `expectType`\n\nutility from libraries like `tsd`\n\nor `expect-type`\n\n:\n\n``` python\nimport { expectType } from 'tsd';\nimport { useRef } from 'react';\nimport Button from './Button';\n\nconst buttonRef = useRef<HTMLButtonElement>(null);\n\n// Should compile without errors\nexpectType<JSX.Element>(<Button ref={buttonRef} variant=\"primary\">Click</Button>);\n\n// Should reject invalid ref types\n// @ts-expect-error\nexpectType<JSX.Element>(<Button ref={useRef<HTMLDivElement>(null)} variant=\"primary\">Click</Button>);\n```\n\nThese type tests run during CI and fail the build if component interfaces drift. The tests verify that `ref`\n\naccepts the correct element type and rejects incompatible types.\n\nFor 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.\n\nThe edge case appears in components that conditionally render different elements based on props. A component that renders either a `button`\n\nor an `a`\n\nelement depending on an `href`\n\nprop must type the ref union correctly:\n\n``` js\nimport { Ref } from 'react';\n\ninterface ButtonLinkProps {\n  href?: string;\n  children: React.ReactNode;\n  ref?: Ref<HTMLButtonElement | HTMLAnchorElement>;\n}\n\nfunction ButtonLink({ href, children, ref }: ButtonLinkProps) {\n  if (href) {\n    return <a ref={ref as Ref<HTMLAnchorElement>} href={href}>{children}</a>;\n  }\n  return <button ref={ref as Ref<HTMLButtonElement>}>{children}</button>;\n}\n```\n\nTesting these components requires separate test cases for each rendering path, verifying that the ref attaches to the correct element type in each scenario.\n\nExisting `forwardRef`\n\nusage 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.\n\nYes, 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.\n\nHOCs accept `ref`\n\nas a standard prop and pass it through to the wrapped component. The HOC's props interface must include `ref`\n\nwith the appropriate element type, and the component must forward it explicitly in the JSX.\n\nNo special handling is required. Generic components accept `ref`\n\nas a prop with the appropriate element type, and TypeScript infers both the generic type parameter and the ref type independently without conflicts.\n\nRuntime tests should render the component with a ref, capture the ref value in a `useEffect`\n\n, and assert that it points to the correct DOM element. Type-level tests using `expectType`\n\nshould verify that the component accepts valid ref types and rejects invalid ones.\n\nThe migration from `forwardRef`\n\nto 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.\n\nThe 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.\n\nThat 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.", "url": "https://wpnews.pro/news/react-20-ref-as-a-prop-migrating-away-from-forwardref-across-a-large-component", "canonical_source": "https://dev.to/jsmanifest/react-20-ref-as-a-prop-migrating-away-from-forwardref-across-a-large-component-library-3jk0", "published_at": "2026-08-20 06:59:05+00:00", "updated_at": "2026-08-20 07:13:50.256041+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["React"], "alternates": {"html": "https://wpnews.pro/news/react-20-ref-as-a-prop-migrating-away-from-forwardref-across-a-large-component", "markdown": "https://wpnews.pro/news/react-20-ref-as-a-prop-migrating-away-from-forwardref-across-a-large-component.md", "text": "https://wpnews.pro/news/react-20-ref-as-a-prop-migrating-away-from-forwardref-across-a-large-component.txt", "jsonld": "https://wpnews.pro/news/react-20-ref-as-a-prop-migrating-away-from-forwardref-across-a-large-component.jsonld"}}