Skip to main content

ref as a Prop: React 19 Eliminates forwardRef

In React 18 and earlier, passing a ref to a custom component required wrapping it with forwardRef—a pattern that felt like a gotcha for new developers and added visual noise to codebases. React 19 simplified this entirely: ref is now a standard prop, just like className or onClick. You no longer need forwardRef, and custom components handle refs the same way they handle any other prop.

This change eliminates an entire category of confusion. Before, developers would hit an error when trying to pass ref to a custom button and have to learn about forwardRef. Now they just pass ref and it works. The simplification ripples through component libraries, reducing boilerplate by thousands of lines across the ecosystem.

The Old Pattern: forwardRef in React 18

In React 18, refs were treated specially. You couldn't pass them directly to custom components:

// React 18: This doesn't work
function CustomInput(props) {
return <input {...props} />;
}

const inputRef = useRef();
<CustomInput ref={inputRef} />; // ❌ ref is undefined in CustomInput

To fix this, you had to use the forwardRef wrapper:

// React 18: forwardRef wrapper required
import { forwardRef } from 'react';

const CustomInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});

const inputRef = useRef();
<CustomInput ref={inputRef} />; // ✓ Works, but requires wrapper

This was confusing because:

  1. Props are automatically passed; ref wasn't (breaking convention)
  2. The signature changed: (props, ref) => ... instead of (props) => ...
  3. You had to remember to use forwardRef before it worked
  4. Every component library had to document this gotcha

The New Pattern: React 19

In React 19, ref is treated like any other prop:

// React 19: No wrapper needed
function CustomInput({ ref, ...props }) {
return <input ref={ref} {...props} />;
}

const inputRef = useRef();
<CustomInput ref={inputRef} />; // ✓ Works immediately

That's it. No forwardRef, no special syntax, no wrapper. The component receives ref as a prop and uses it like anything else. This aligns with how developers intuitively expected refs to work.

Practical Example: Building a Custom Form Component

Here's a realistic custom input component that wraps HTML input with validation and styling:

import { useState, useRef } from 'react';

function CustomInput(
{
ref,
label,
error,
required,
type = 'text',
...props
}
) {
const [focused, setFocused] = useState(false);

return (
<div className={`form-group ${error ? 'error' : ''}`}>
{label && (
<label htmlFor={props.id}>
{label}
{required && <span className="required">*</span>}
</label>
)}
<input
ref={ref}
type={type}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
className={`input ${focused ? 'focused' : ''}`}
{...props}
/>
{error && <span className="error-message">{error}</span>}
</div>
);
}

// Usage: ref works immediately, no forwardRef needed
export function SignupForm() {
const emailRef = useRef();
const passwordRef = useRef();

const handleSubmit = (e) => {
e.preventDefault();
console.log('Email:', emailRef.current.value);
console.log('Password:', passwordRef.current.value);
};

return (
<form onSubmit={handleSubmit}>
<CustomInput
ref={emailRef}
label="Email"
type="email"
required
/>
<CustomInput
ref={passwordRef}
label="Password"
type="password"
required
/>
<button type="submit">Sign Up</button>
</form>
);
}

The custom input accepts ref as a prop, passes it to the underlying input, and provides additional features (validation styling, labels, error messages). Callers can now access the DOM element directly without ceremony.

Refs with Function Components and Hooks

Function components in React 19 can receive and use refs naturally. This is particularly useful for components that expose imperative methods:

import { useRef, useImperativeHandle } from 'react';

function TextEditor({ ref, initialValue = '' }) {
const internalRef = useRef(initialValue);

useImperativeHandle(ref, () => ({
focus: () => {
internalRef.current.focus();
},
clear: () => {
internalRef.current.value = '';
},
getText: () => {
return internalRef.current.value;
},
}));

return (
<textarea
ref={internalRef}
defaultValue={initialValue}
className="editor"
/>
);
}

// Usage: call methods on the ref handle
export function App() {
const editorRef = useRef();

const handleSave = () => {
const text = editorRef.current.getText();
console.log('Saving:', text);
};

return (
<>
<TextEditor ref={editorRef} initialValue="Start typing..." />
<button onClick={() => editorRef.current.focus()}>Focus</button>
<button onClick={handleSave}>Save</button>
</>
);
}

useImperativeHandle still exists in React 19 and works the same way—it lets you expose custom methods on a ref. Now you can use it without the forwardRef wrapper.

Migrating from forwardRef to React 19

Updating existing code is straightforward. For each forwardRef component:

  1. Remove the forwardRef wrapper
  2. Destructure ref from props in the function signature
  3. Pass ref to the underlying element

Before:

import { forwardRef } from 'react';

const Button = forwardRef(({ label, ...props }, ref) => {
return <button ref={ref} {...props}>{label}</button>;
});

After:

function Button({ ref, label, ...props }) {
return <button ref={ref} {...props}>{label}</button>;
}

If you have a large codebase, this is easy to automate with a find-and-replace or codemod. The behavior is identical; you're just removing the wrapper.

Refs and TypeScript

TypeScript users get cleaner signatures without the ForwardedRef type:

interface CustomInputProps {
ref?: React.RefObject<HTMLInputElement>;
label: string;
required?: boolean;
}

function CustomInput({ ref, label, required }: CustomInputProps) {
return (
<div>
<label>{label}</label>
<input ref={ref} />
</div>
);
}

No need for React.forwardRef<HTMLInputElement, CustomInputProps> or ForwardedRef types. ref is just another prop with a standard type.

Key Takeaways

  • React 19 treats ref as a standard prop, eliminating the need for forwardRef.
  • Custom components now receive ref directly in their function signature.
  • Migration is simple: remove the forwardRef wrapper and destructure ref from props.
  • This change simplifies component libraries, documentation, and onboarding for new developers.
  • useImperativeHandle still works (and is still optional) for exposing custom methods on refs.

Frequently Asked Questions

Do I have to update all my forwardRef components immediately?

No. forwardRef still works in React 19 for backward compatibility. You can migrate incrementally—use the new pattern for new components and update existing ones when you touch them for other reasons.

What if a library I use still uses forwardRef?

It will work fine with React 19. The forwardRef wrapper is transparent; it doesn't break anything. Once the library updates, you'll be able to use refs directly with their components.

Can I use ref in combination with other props like children?

Yes. ref is just another prop. You can destructure it alongside children, className, or anything else: function Button({ ref, children, className, ...props }).

Does this work with all component types?

Yes. Function components, class components (in rare cases), and custom hook-based components all work the same way. ref is universally supported.

What about class components?

Class components still use ref the same way as React 18. The ref prop works directly (no forwarding needed). React 19 doesn't change class component refs, only function components.

Further Reading