Migrating existing forwardRef components and staying compatible in mixed codebases
React 19 removes one of the framework's oldest special cases: ref can now be received directly as a regular prop, no forwardRef wrapper required. This article walks through the new pattern, how to migrate existing forwardRef components, TypeScript typing, and how both patterns coexist safely in a mixed codebase.
Table of Contents
- 1. What Changed for refs in React 19
- 2. The New Pattern: Receiving ref as a Plain Prop
- 3. Why forwardRef Was Needed in the First Place
- 4. Migrating Existing forwardRef Components
- 5. Typing It in TypeScript
- 6. Backward Compatibility in Mixed Codebases
- 7. Combining It with useImperativeHandle
- 8. Linting Pitfalls and ref Cleanup Functions
- 9. When Migration Is Worth It
- 10. Summary
- 11. FAQ
1. What Changed for refs in React 19
Through React 18, ref held a special status among props: anyone building a function component and wanting to access an internal DOM element or an imperative handle from outside via ref had to wrap the component in forwardRef. A plain function call like function MyInput(props) { ... } could not receive a ref prop at all, React stripped it out of props internally before the function was even invoked.
React 19 lifts this restriction: ref is now passed straight through to the function component just like any other prop, with no forwardRef wrapper needed. This noticeably cuts boilerplate, simplifies TypeScript typing, and turns component definitions back into plain functions without a special case. For new components, forwardRef has therefore become unnecessary in most situations.
2. The New Pattern: Receiving ref as a Plain Prop
The change at the code level is unspectacular: ref is simply listed as another field in the props destructuring, exactly like any other value. Inside the component, the ref can then be forwarded to a DOM element the usual way, either via useEffect or directly in JSX, exactly as you would with any regular prop.
This simplified pattern pays off immediately, especially for small, reusable UI building blocks like an input field or a button, because the previous forwardRef wrapper along with its second callback layer disappears entirely. The component stays a single, flat function, which also makes debugging in the React DevTools tree clearer, since no extra ForwardRef layer shows up in the component tree anymore.
function TextInput({ label, ref, ...props }) {
return (
<label>
{label}
<input ref={ref} {...props} />
</label>
);
}
function Form() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current?.focus();
};
return (
<>
<TextInput label="Name" ref={inputRef} />
<button onClick={focusInput}>Focus</button>
</>
);
}
3. Why forwardRef Was Needed in the First Place
The historical reason for forwardRef lies in React's reconciliation model: ref was never an ordinary data flow from parent to child component, but an imperative side channel through which React reports back a DOM reference or an instance directly. To prevent this side channel from accidentally being passed through the component as a regular prop, potentially altered or ignored along the way, React consistently stripped ref out of the props object.
forwardRef gave component authors an explicit second function signature, (props, ref) => ..., through which that side channel was deliberately made accessible again. This worked reliably, but meant every reusable base component had to apply this wrapper preemptively, even when it was not yet clear at creation time whether ref would ever be needed, which in practice led to a lot of defensive boilerplate.
4. Migrating Existing forwardRef Components
Migrating a single component follows three small steps: remove the forwardRef call, merge the second function signature (props, ref) into a flat props destructuring with ref as an additional field, and keep the component name as a plain function or const declaration. The return value and the JSX inside the component body stay unchanged, only the outer function shell changes.
For larger codebases with many base components, a gradual migration component by component is preferable to one large rewrite, since forwardRef continues to work in React 19 and both patterns can coexist without issue. A good starting point is heavily used but simple components like buttons or input fields, where the effort per migration amounts to just a few lines.
// Before: React 18 pattern
const Button = forwardRef(function Button(props, ref) {
return <button ref={ref} className="btn" {...props} />;
});
// After: React 19 pattern
function Button({ ref, ...props }) {
return <button ref={ref} className="btn" {...props} />;
}
5. Typing It in TypeScript
Under React 18, a typed forwardRef component had to pass the ref type and the props type as two separate generics to forwardRef
This not only simplifies the signature, it also makes generic, reusable components easier to type, since there is no separate ForwardRefExoticComponent construct to manage around the actual component anymore. For components meant to support multiple element types, say polymorphic buttons, the ref type can additionally be tied to the element kind through a generic type parameter.
import { Ref } from "react";
interface TextInputProps {
label: string;
ref?: Ref<HTMLInputElement>;
placeholder?: string;
}
function TextInput({ label, ref, placeholder }: TextInputProps) {
return (
<label>
{label}
<input ref={ref} placeholder={placeholder} />
</label>
);
}
6. Backward Compatibility in Mixed Codebases
React 19 marks forwardRef as deprecated but does not remove it; existing code using forwardRef keeps running unchanged. This matters especially for projects pulling in third-party UI libraries whose own components still use forwardRef internally, since those libraries will only gradually adopt the new React 19 pattern themselves, and updating them is not within the project's own control.
In practice this means a single codebase can contain both patterns side by side for a long time without any functional problems: a component built with forwardRef can be used without issue inside a component that already uses the new ref-as-prop pattern, and vice versa. A team can therefore prioritize migration calmly instead of rewriting every component under time pressure.
7. Combining It with useImperativeHandle
Components that want to expose a curated, imperative API instead of a raw DOM element, say only a focus() and a reset() method instead of the full input element, still use useImperativeHandle for that. The hook itself has not changed functionally, but under React 19 it no longer needs forwardRef as a carrier component and can be used directly inside a plain function component with a ref prop.
This trims the boilerplate for imperative component APIs down to the essentials: props destructuring with ref, a useRef for the internal element, and a useImperativeHandle call that exposes the desired methods on the forwarded ref. For more complex form components with several imperative methods, this pattern stays considerably clearer than the two-layer forwardRef version from React 18.
function FancyInput({ ref, ...props }) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
clear: () => {
if (inputRef.current) inputRef.current.value = "";
},
}));
return <input ref={inputRef} {...props} />;
}
8. Linting Pitfalls and ref Cleanup Functions
Older ESLint configurations using the react-hooks plugin rule may not yet recognize the new ref-as-prop pattern and incorrectly flag ref as an unused or misplaced prop; updating to a current plugin version reliably fixes this. Another pitfall is the order of props destructuring: ref should never accidentally end up part of a rest spread that gets passed unfiltered to an underlying element, or ref ends up duplicated in the DOM.
React 19 also introduces ref cleanup functions: a ref callback can now itself return a function that gets called automatically on unmount or when the ref changes, closely mirroring the familiar useEffect cleanup pattern. Combined with ref as a plain prop, setup and teardown logic for a DOM element, say registering and removing a native event listener, can be attached directly to the ref callback itself without an additional useEffect.
9. When Migration Is Worth It
For new components there is practically no reason left to use forwardRef, the new pattern is shorter, easier to type, and functionally identical. For existing components, priority depends on how often they get touched: components already being reworked can be migrated in the same pass, while stable, rarely changed components can happily keep running with forwardRef.
A runtime or bundle size difference between the two patterns is practically unmeasurable, the benefit of migrating lies entirely in readability, simpler typing, and less boilerplate, not in performance. Teams should therefore treat the migration as a code quality measure that fits naturally into refactors already planned, rather than as an isolated, urgent task.
| Aspect | forwardRef (React 18) | ref as a prop (React 19) | Assessment |
|---|---|---|---|
| Boilerplate | Extra wrapper and second signature | Plain function component | Less code in React 19 |
| TypeScript typing | forwardRef |
ref directly in the props interface | Simpler in React 19 |
| Compatibility | Works in React 18 and 19 | React 19 only | forwardRef needed for React 18 support |
| useImperativeHandle | Usable only inside forwardRef | Directly in a plain component | Less nesting in React 19 |
| Runtime behavior | Identical | Identical | No performance difference |
Mironsoft
React architecture, performance, and Magento frontend integration
React frontends that stay fast instead of slowing down with every feature?
We review existing React applications for unnecessary re-renders, bloated bundles, and fragile state management, then build a frontend that stays performant and connects cleanly to Magento or other backends.
Performance Audit
Systematically measuring and fixing re-renders, bundle size, and load times.
State Architecture
Cleanly separating context, client state, and server state instead of mixing everything.
Magento Integration
Building robust, type-safe GraphQL or REST integration with Magento.
10. Summary
ref as a Prop in React 19: The Essentials at a Glance
Core change
ref is passed straight through to function components like any other prop in React 19, forwardRef has become optional.
Migration
Remove the wrapper, add ref to the props destructuring, the component body stays unchanged.
Compatibility
forwardRef keeps working, mixed codebases using both patterns coexist without issue.
TypeScript
ref can be typed directly in the props interface, the cumbersome forwardRef generics pair disappears.