//
Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
// React 19: ref is just a prop
function TextInput({ placeholder, ref }: { placeholder: string; ref?: React.Ref<HTMLInputElement> }) {
return <input placeholder={placeholder} ref={ref} />;
}
// Usage -- identical to before
import { useRef } from "react";
function App() {
const inputRef = useRef<HTMLInputElement>(null);
return (
<div>
<TextInput ref={inputRef} placeholder="Type here..." />
<button onClick={() => inputRef.current?.focus()}>Focus</button>
</div>
);
}When to reach for this: Use this pattern for any function component that needs to expose a DOM node or imperative handle to a parent. It replaces forwardRef entirely.
// A reusable input component library with ref support
"use client";
import { useRef, useImperativeHandle, type Ref, type ReactNode } from "react";
// Simple ref forwarding -- no forwardRef wrapper needed
function Input({
label,
error,
ref,
...inputProps
}: {
label: string;
error?: string;
ref?: Ref<HTMLInputElement>;
} & React.InputHTMLAttributes<HTMLInputElement>) {
return (
<div className="flex flex-col gap-1">
<label className="text-sm font-medium">{label}</label>
<input
ref={ref}
className={`border rounded p-2 ${error ? "border-red-500" : "border-gray-300"}`}
{...inputProps}
/>
{error && <span className="text-red-500 text-sm">{error}</span>}
</div>
);
}
// Imperative handle with ref -- also no forwardRef needed
type ComboboxHandle = {
open: () => void;
close: () => void;
reset: () => void;
};
function Combobox({
options,
ref,
}: {
options: string[];
ref?: Ref<ComboboxHandle>;
}) {
const inputRef = useRef<HTMLInputElement>(null);
const [isOpen, setIsOpen] = useState(false);
const [value, setValue] = useState("");
useImperativeHandle(ref, () => ({
open: () => setIsOpen(true),
close: () => setIsOpen(false),
reset: () => {
setValue("");
setIsOpen(false);
},
}));
return (
<div className="relative">
<input
ref={inputRef}
value={value}
onChange={(e) => setValue(e.target.value)}
onFocus={() => setIsOpen(true)}
/>
{isOpen && (
<ul className="absolute top-full left-0 border bg-white w-full">
{options
.filter((o) => o.toLowerCase().includes(value.toLowerCase()))
.map((option) => (
<li
key={option}
onClick={() => {
setValue(option);
setIsOpen(false);
}}
className="p-2 hover:bg-gray-100 cursor-pointer"
>
{option}
</li>
))}
</ul>
)}
</div>
);
}
// Parent using both components
import { useState } from "react";
export default function RegistrationForm() {
const nameRef = useRef<HTMLInputElement>(null);
const comboRef = useRef<ComboboxHandle>(null);
function handleReset() {
nameRef.current?.focus();
comboRef.current?.reset();
}
return (
<form className="space-y-4">
<Input ref={nameRef} label="Full Name" placeholder="Jane Doe" />
<Combobox ref={comboRef} options={["Engineering", "Design", "Product", "Marketing"]} />
<button type="button" onClick={handleReset}>
Reset & Focus Name
</button>
</form>
);
}What this demonstrates:
Input component receiving ref as a regular prop and forwarding it to the native <input>Combobox component using useImperativeHandle with ref as a prop to expose custom methodsuseRef to interact with child DOM nodes and imperative handlesforwardRef wrapper anywhereref is treated as a regular prop for function components. React no longer strips it out before passing props to the component.forwardRef still works and is not removed, but it is deprecated. A future React version will remove it. React logs a deprecation warning in development mode.ref prop is included in the component's props type automatically when using React.ComponentProps<typeof MyComponent>.ref as before -- the ref points to the class instance. This change only affects function components.useImperativeHandle(ref, createHandle) continues to work exactly as before. The only difference is where ref comes from -- a prop instead of the second argument of forwardRef.Ref cleanup functions (new in React 19):
function MeasuredDiv({ ref }: { ref?: Ref<HTMLDivElement> }) {
return (
<div
ref={(node) => {
// Setup
if (node) {
console.log("Mounted:", node.getBoundingClientRect());
}
// Return cleanup function (new in React 19)
return () => {
console.log("Unmounted");
};
}}
>
Measured content
</div>
);
}Generic component with ref:
type ListProps<T> = {
items: T[];
renderItem: (item: T) => ReactNode;
ref?: Ref<HTMLUListElement>;
};
function List<T>({ items, renderItem, ref }: ListProps<T>) {
return (
<ul ref={ref}>
{items.map((item, i) => (
<li key={i}>{renderItem(item)}</li>
))}
</ul>
);
}Migration from forwardRef:
// Before (React 18)
const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
return <input ref={ref} {...props} />;
});
// After (React 19)
function Input({ ref, ...props }: InputProps & { ref?: Ref<HTMLInputElement> }) {
return <input ref={ref} {...props} />;
}Ref<T> (not RefObject<T>) for the prop type: ref?: React.Ref<HTMLInputElement>.React.Ref<T> is a union of RefCallback<T>, RefObject<T>, and null. This covers all ref usage patterns.ComponentProps, the ref prop is included automatically in React 19 types.Ref<MyHandle> where MyHandle is your custom interface.forwardRef code works but logs a warning. Fix: Migrate by moving ref from the second forwardRef argument into the props object. Run the React codemod: npx @react-codemod/cli forward-ref-as-prop.{...props} now includes ref if the parent passed one. Fix: Destructure ref explicitly: const { ref, ...rest } = props.undefined, React will warn. Fix: Ensure ref callbacks either return nothing or return a cleanup function.ref on a class component still points to the instance, not a prop. Fix: No migration needed for class components.forwardRef internally continue to work. You do not need to wait for them to migrate. Fix: No action required.| Approach | When to choose |
|---|---|
ref as prop (React 19) | Default for all new function components |
forwardRef (deprecated) | Existing code that has not been migrated yet |
useImperativeHandle | When you need to expose a custom API instead of a DOM node |
| Callback ref | When you need to run logic on mount/unmount of the ref target |
| State-based communication | When parent-child communication should trigger re-renders instead of imperative calls |
forwardRef added unnecessary wrapper complexity and made component APIs harder to understandref is treated as a regular prop for function components -- React no longer strips it outforwardRef still works but is deprecated and will be removed in a future version// Before (React 18)
const Input = forwardRef<HTMLInputElement, Props>((props, ref) => {
return <input ref={ref} {...props} />;
});
// After (React 19)
function Input({ ref, ...props }: Props & { ref?: Ref<HTMLInputElement> }) {
return <input ref={ref} {...props} />;
}useImperativeHandle(ref, createHandle) works exactly as beforeref comes from props instead of the second argument of forwardRefopen(), close(), reset()) to parent components<div ref={(node) => {
if (node) console.log("Mounted:", node);
// Return cleanup function (new in React 19)
return () => console.log("Unmounted");
}} />Ref callbacks can now return a cleanup function, similar to useEffect cleanup.
ref as before -- the ref points to the class instancenpx @react-codemod/cli forward-ref-as-prop to automatically migrateref from the forwardRef second argument into the props destructuringtype ListProps<T> = {
items: T[];
renderItem: (item: T) => ReactNode;
ref?: Ref<HTMLUListElement>;
};
function List<T>({ items, renderItem, ref }: ListProps<T>) {
return (
<ul ref={ref}>
{items.map((item, i) => <li key={i}>{renderItem(item)}</li>)}
</ul>
);
}{...props} now includes ref if the parent passed one, which may forward it unintentionallyref explicitly: const { ref, ...rest } = propsref only where intendedundefinedReact.Ref<T> (not RefObject<T>) for the prop type: ref?: React.Ref<HTMLInputElement>React.Ref<T> is a union of RefCallback<T>, RefObject<T>, and nullReact.ComponentProps<typeof MyComponent> automatically includes ref in the typeref manually when extending component propsref was excluded from ComponentPropsforwardRef internally continue to work without issuesforwardRef is deprecated but functionalReviewed by Chris St. John·Last updated Jul 16, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥