The Retirement of forwardRef and useImperativeHandle: Now, ref is Just a Prop

Chris is building a signup form.
He wants to implement a feature where, if a user clicks "Check for Duplicates" and the email is already taken, the email input field (CustomInput) turns red and automatically receives focus.
Since the parent component needs access to the child's input, Chris decided to pass a ref, just as he learned.
// Parent.tsx
function Parent() {
const inputRef = useRef<HTMLInputElement>(null);
const handleClick = () => {
inputRef.current?.focus(); // 💥 Error: current is undefined?
};
return <CustomInput ref={inputRef} />;
}
// CustomInput.tsx
// ❌ Traditional approach: ref is not passed as a prop.
function CustomInput(props) {
return <input ref={props.ref} />; // props.ref is undefined.
}A red warning appears in the console: "Function components cannot be given refs..."
This is because in React, ref (along with key) is a reserved word treated specially and is not passed as a standard prop.
To solve this, we historically had to use a complex Higher-Order Component (HOC) called forwardRef. However, with the arrival of React 19, this long-standing inconvenience is finally fading into history.
1. A Relic of the Past: The Inconvenience of forwardRef
Until React 18, if a child component wanted to receive a ref, it absolutely had to be wrapped in forwardRef.
// ⚠️ React 18 and below: forwardRef is mandatory
import { forwardRef } from 'react';
const CustomInput = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
return <input ref={ref} {...props} />;
});
// There was also the hassle of setting displayName for debugging because the component name got lost.
CustomInput.displayName = 'CustomInput';This method was uncomfortable for several reasons:
2. The React 19 Revolution: ref is Now Just a Prop
The React 19 team resolved this inconsistency. ref is no longer a special reserved word. It has become just a part of props, like className or onClick.
Now, Chris can delete forwardRef and pull ref directly from the function arguments.
// ✅ React 19: Just receive and use it.
function CustomInput({ ref, ...props }: { ref: React.Ref<HTMLInputElement> }) {
return <input ref={ref} {...props} />;
}How beautiful! The wrapper shell around the component is gone, and the code has become intuitive. This aligns with the direction of "simplicity" that the React team is pursuing.
3. useImperativeHandle: Showing Parents Only What You Want
When you pass a ref, the parent gets the entire DOM node (HTMLInputElement) of the child.
This means the parent can arbitrarily hide the child via input.style.display = 'none' or force-change its value. This is dangerous behavior that breaks encapsulation.
"I want to expose only the focus() function to the parent and hide the rest."
The hook used for this purpose is useImperativeHandle. It allows you to customize the content of the ref that the parent receives.
How to Implement
Using useImperativeHandle, instead of the DOM node, a custom object (Handle) that we create is stored in the parent's ref.current.
// Child Component
import { useImperativeHandle, useRef } from 'react';
// 1. Define the type of methods to expose to the parent
export interface InputHandle {
focus: () => void;
shake: () => void; // Shake animation on error
}
function CustomInput({ ref, ...props }: { ref: React.Ref<InputHandle> }) {
const internalRef = useRef<HTMLInputElement>(null);
// 2. Connect (Handle) the object I made to the parent's ref.
useImperativeHandle(ref, () => {
return {
// Expose only the focus method of the actual input DOM
focus: () => internalRef.current?.focus(),
// Add my own custom method
shake: () => {
internalRef.current?.classList.add('shake');
setTimeout(() => internalRef.current?.classList.remove('shake'), 500);
}
};
});
return <input ref={internalRef} {...props} />;
}Usage in Parent Component
Now, the parent cannot touch the child's input tag itself. It can only use the focus and shake functions permitted by the child.
// Parent.tsx
function Parent() {
const inputRef = useRef<InputHandle>(null);
const handleError = () => {
// DOM manipulation is impossible, but defined functions are usable
inputRef.current?.focus();
inputRef.current?.shake();
};
return <CustomInput ref={inputRef} />;
}This pattern is useful when creating video player components (exposing only play and pause) or delegating control of complex animations to the parent.
4. When Should You Use This?
The cardinal rule of React is "Data must flow unidirectionally (Parent -> Child)." Control via ref is an "Escape Hatch" that goes against this principle.
If it can be solved declaratively (props), it is better to avoid the imperative way (ref).
Key Takeaways
Handling ref has become much cleaner. But React 19's innovation doesn't stop here.
The restriction of having to declare hooks at the top level of a component every time you use useContext is also disappearing. What if you could read Context even inside a conditional statement?
Continuing in: "use API: The Evolution of useContext and Handling Promises (Suspense)."