React 19 makes ref
easier – just pass it!
React 19 brings many exciting updates, and one of the simplest yet powerful change is how ref
works. We no longer need forwardRef
when passing ref
to built-in elements like inputs. Cool, right?
This small update makes working with ref
much simpler, especially in forms where smooth user interactions matter.
The problem with forwardRef
Before React 19, if a parent component wanted to interact with a child component using a ref, we had to wrap the child in forwardRef
. Annoying, right?
It made the code harder to read and maintain, especially in large projects.
Thankfully, React 19 makes ref handling much easier.
Let’s check out why ref
became smarter than before with an example!
Understanding the change with a pizza example
Imagine we have a Pizza Order form with reusable components for selecting pizza type and size. Now, what if selecting a pizza automatically moves the cursor to the size selection field? That would make ordering even smoother, right?
Before React 19, we had to use forwardRef
to achieve this behavior.
Before React 19 (using forwardRef
– complex)
import React, { useRef, forwardRef } from "react";
const SelectBox = forwardRef(({ options, onChange }, ref) => (
<select ref={ref} onChange={onChange}>
{options.map((option) => (
<option key={option} value={option}>{option}</option>
))}
</select>
));
const App = () => {
const typeRef = useRef(null);
const sizeRef = useRef(null);
useEffect(() => {
console.log("Refs assigned");
return () => {
console.log("Cleaning up refs");
typeRef.current = null;
sizeRef.current = null;
};
}, []);
return (
<div className="App">
<h1> Pizza Form </h1>
options={["Select a pizza", "Margherita", "Pepperoni"]}
onChange={() => sizeRef.current.focus()}
ref={typeRef}
/>
<SelectBox options={["Select size", "Small", "Medium", "Large"]}
ref={sizeRef}
/>
</div>
);
}
Here, we had to use forwardRef
just to pass the ref
. A bit too much, right?
Now, let’s see how React 19 simplifies this process and makes our code much cleaner!
After React 19 (passing ref
as a prop - easier)
import React, { useRef } from "react";
const SelectBox = ({ options, onChange, ref }) => (
<select ref={ref} onChange={onChange}>
{options.map((option) => (
<option key={option} value={option}>{option}</option>
))}
</select>
);
function App() {
const typeRef = useRef(null);
const sizeRef = useRef(null);
useEffect(() => {
console.log("Refs assigned");
return () => {
console.log("Cleaning up refs");
typeRef.current = null;
sizeRef.current = null;
};
}, []);
return (
<div className="App">
<h1> Pizza Form </h1>
<SelectBox
options={["Select a pizza", "Margherita", "Pepperoni"]}
ref={typeRef}
onChange={()=> sizeRef.current.focus()}
/>
<SelectBox options={["Select size", "Small", "Medium", "Large"]}
ref={sizeRef}
/>
</div>
);
}
And just like that, we pass ref as a normal prop, no extra hassle!
Why this Update is useful
Less Boilerplate: No more unnecessary forwardRef wrapping.
More Readable: Code is easier to follow and understand.
Easier Maintenance: Ref interactions are much cleaner.
Better Composition: Ref handling now feels more natural, perfect for quick, user-friendly forms and smooth transitions.
Cleanup function for ref
React 19 takes ref management a step further. It now allows us to return a cleanup function from ref callbacks, making it easier to handle cleanups when components unmount. With this update, callback refs automatically handle cleanup when the component is removed from the DOM, which reduces the need for manual cleanup code like useEffect
to manage clean up of ref
.
Let’s update the Pizza example to understand this change:
import React, { useState } from "react";
const SelectBox = ({ options, onChange, ref }) => {
return (
<select ref={ref} onChange={onChange}>
{options.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
);
};
export default function App() {
let typeRef = null;
let sizeRef = null;
const setTypeRef = (ref) => {
if (ref) {
console.log("setup typeref");
typeRef = ref;
}
return () => {
console.log("Cleanup function called for type ref");
typeRef = null; // Cleanup previous ref
};
};
const setSizeRef = (ref) => {
if (ref) {
console.log("setup sizeref");
sizeRef = ref;
}
return () => {
console.log("Cleanup function called for size ref");
sizeRef = null; // Cleanup previous ref
};
};
return (
<div className="App">
<h1> Pizza Form </h1>
<SelectBox
options={["Select a pizza", "Margherita", "Pepperoni"]}
ref={setTypeRef}
onChange={() => {
sizeRef?.focus();
}}
/>
<SelectBox
ref={setSizeRef}
onChange={() => {
typeRef?.focus();
}}
options={["Select size", "Small", "Medium", "Large"]}
/>
</div>
);
}
Ref cleanup process:
Previously, React would call ref functions with null
when unmounting the component. With this update, if the ref returns a cleanup function, React will simply skip this step. In future versions, calling refs with null when unmounting components will be deprecated.
TypeScript rejection:
With the introduction of ref cleanup functions, TypeScript now rejects any return values other than null
or undefined
from a ref callback. This fix is introduced to avoid using implicit returns, for example:
//before
const myRef = (node) => node && node.focus(); // Implicit return not allowed
//after
const myRef = (node: HTMLDivElement | null) => {
if (node) {
node.focus();
} //Only explicit return is accepted by typescript
};
By using explicit returns, we ensure our code follows both TypeScript and React 19’s rules, reducing risk of errors and improving ref handling.
Conclusion
With React 19, refs
feel like a natural part of our components, more clarity, and a better developer experience!
Also the introduction of automatic cleanup for callback refs ensures smooth handling of component states and side effects during unmounting, improving reliability and reducing potential issues.