React 19 Ref Updates - Prop Support & Cleanup Function!

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.

Need help on your Ruby on Rails or React project?

Join Our Newsletter