React 19.2 ref as Prop Breaking forwardRef Libraries Fix

React 19.2 ref-as-prop colliding with legacy forwardRef components from your UI library? Here is why the double-ref bug fires and the wrapper that fixes it.
React 19.2 ref as Prop Breaking forwardRef Libraries Fix
ReactNext.jsTypeScript
June 13, 20266 min read1160 words

The Problem

I ran into this on a Next.js 16 client project that still depends on an older component library. After bumping React from 19.0 to 19.2, half the form fields stopped accepting focus and a quarter of them stopped rendering at all. The React DevTools tree looked correct. The terminal showed no error. But the browser console was loud:

Warning: Function components cannot be given refs. Attempts to access this ref will fail.
Did you mean to use React.forwardRef()?

  in Input (created by FormField)
  in FormField (created by SettingsForm)

That warning is the old React 16 message, which made it confusing, since the library in question was already wrapping Input in forwardRef. Reading more carefully, the warning was firing on a different component each render. Some renders it was Input, others it was the Tooltip from a different vendor, others it was a Modal from yet another. Production was worse than dev: about 8% of users hit an undefined is not an object (evaluating 'ref.current') crash on the first interaction with a form.

The pattern was the same in every case. A parent component passed a ref to a child. The child used React.forwardRef. React 19.2 was refusing to forward it.

Why It Happens

React 19 made ref a regular prop on function components. You can now write <MyComponent ref={myRef} /> without forwardRef, and the ref shows up alongside the rest of the props inside the component. That change shipped in 19.0 with a backwards-compatible path: components that still used forwardRef kept working.

19.2 tightened the path. The runtime now treats ref as a reserved prop name in two new places. First, when a child component is wrapped in React.memo, React 19.2 unwraps the forwardRef automatically so it can hand the ref through as a prop. Second, when a parent passes ref via {...props} spread (which most third-party libraries do internally), 19.2 strips the ref from the spread and re-injects it as a top-level argument to the function component.

The first change is fine. The second one is where existing libraries break. If a library's forwardRef wrapper expected ref to arrive as the second argument and the parent uses {...props} to merge in additional refs, the merge produces a ref key inside props while the second argument is null. The component then either silently drops the ref (focus stops working) or, worse, tries to call .current on something that is undefined.

The other half of the bug is TypeScript. The RefObject<T> type lost its second null overload in 19.2's @types/react, so libraries built against @types/react@^19.0.0 now expose a return type that no longer matches what React's runtime delivers. The runtime breakage is the cause. The type breakage is the reason most teams notice it during the next CI run, not during the upgrade.

The React 19.2 upgrade notes cover the API change but do not mention the spread-merge interaction with forwardRef.

The Fix

Three layers. Apply them in order — fixing the type alone will not fix the runtime crash, and fixing the runtime alone will leave CI red.

Layer 1: Wrap the offending library component so the ref arrives as a prop. Do not modify the library. Create a thin local wrapper that converts the forwardRef signature to the 19.2 ref-as-prop signature in one place:

import { Input as VendorInput } from 'vendor-ui'
import type { ComponentProps, Ref } from 'react'

type InputProps = ComponentProps<typeof VendorInput> & {
  ref?: Ref<HTMLInputElement>
}

export function Input({ ref, ...props }: InputProps) {
  return <VendorInput {...props} ref={ref} />
}

The wrapper accepts ref as a regular prop, then passes it down explicitly. React 19.2 sees a single, top-level ref and routes it correctly. The spread-merge ambiguity disappears because there is no spread containing a ref.

Layer 2: Fix the TypeScript regression at the boundary. If your project bumped @types/react to 19.2 but a dependency still ships types built against 19.0, the RefObject<HTMLInputElement> from the dependency is not assignable to the Ref<HTMLInputElement> your wrapper expects. Add a Ref cast in the wrapper file, scoped to the one place it is needed:

import type { Ref } from 'react'

type LegacyRef<T> = { current: T | null } | ((node: T | null) => void)

function asRef<T>(legacy: LegacyRef<T> | undefined): Ref<T> | undefined {
  return legacy as Ref<T> | undefined
}

export function Input({ ref, ...props }: InputProps) {
  return <VendorInput {...props} ref={asRef(ref)} />
}

That single cast lets you upgrade React types without waiting on every dependency to re-publish. It is also auditable: asRef is a search-friendly name when you want to remove the shim later.

Layer 3: Catch the spread-merge bug in CI before the next library upgrade reintroduces it. Add an ESLint rule that flags any forwardRef call inside the project's own code:

// eslint.config.mjs
export default [
  {
    rules: {
      'no-restricted-syntax': [
        'error',
        {
          selector: "CallExpression[callee.object.name='React'][callee.property.name='forwardRef']",
          message: 'Use ref-as-prop in React 19.2. See /components/wrappers/README.md',
        },
        {
          selector: "ImportSpecifier[imported.name='forwardRef']",
          message: 'Use ref-as-prop in React 19.2. See /components/wrappers/README.md',
        },
      ],
    },
  },
]

The lint catches direct usage in app code. The wrappers are the only place a vendor forwardRef should be reached, and the wrappers do not call forwardRef themselves.

Verify the runtime fix. The fastest end-to-end check is a focus loop on the rebuilt form. Open the page, tab through every field, then run a quick assertion in the console:

document.querySelectorAll('input, textarea, select').forEach(el => {
  el.focus()
  console.assert(document.activeElement === el, 'Focus lost on', el.name)
})

Every input should accept focus. If any field fails the assertion, the wrapper for that component was missed. Sentry will also stop reporting the undefined ref.current crash within a release or two.

The Lesson

React 19.2 routes ref as a prop and strips it from spreads before invoking the component. Libraries that still call forwardRef and use internal {...props} merging break in subtle ways: dropped focus, undefined refs, occasional production crashes. Wrap each offender in a one-file shim that accepts ref as a regular prop, scope a TypeScript cast to the boundary, and lint the project to keep forwardRef from creeping back in.

If your React 19.2 upgrade has stalled because the UI library is mid-migration, that is a job I get paid for. See my services. For a related ref-and-state regression after the same upgrade, read React useOptimistic not resetting after server action.

Stuck on a React 19.2 upgrade with a legacy UI library? Get it shipped.

Back to blogStart a project