DEV Community

Cover image for Why There’s No Need for forwardRef in React
Sharoz Tanveer🚀
Sharoz Tanveer🚀

Posted on

Why There’s No Need for forwardRef in React

Introduction

Understanding why forwardRef exists and whether it’s truly necessary in React has been a complex journey. After identifying seven significant issues with forwardRef and recognising a simpler and superior alternative, it became evident that forwardRef could be removed from React without much impact. In fact, there’s already an open RFC to remove it.

Understanding ref in React

When we pass a ref to a native HTML element like an <input>, it attaches automatically to the DOM node. We gain access to the native DOM API for this node.

import React, { useRef, useEffect } from 'react';

const App: React.FC = () => {
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(()=>{
    if (inputRef.current) {
    // Accessing the DOM node
      inputRef.current.focus();
    }
  }, []);

  return <input ref={inputRef} />;
};
Enter fullscreen mode Exit fullscreen mode

For class components, the ref attaches to the instance of the class, allowing us to access its internal properties and methods. However, for functional components, the ref results in a null value and a warning. To make ref work with functional components, we wrap them in the forwardRef API:

import React, { forwardRef, useRef } from 'react';

const Child = forwardRef<HTMLDivElement, {}>((props, ref) => (
  <div ref={ref}>Child Component</div>
));

const App: React.FC = () => {
  const childRef = useRef<HTMLDivElement>(null);

  return <Child ref={childRef} />;
};
Enter fullscreen mode Exit fullscreen mode

The Problems with forwardRef

  • Lack of Support for Multiple Refs: forwardRef only allows one argument, making it cumbersome to handle multiple refs without workarounds. For example:
import React, { forwardRef, Ref, useImperativeHandle, useRef } from 'react';
interface FormHandles {
  inputRef1: Ref<HTMLInputElement>;
  inputRef2: Ref<HTMLInputElement>;
}

const Form = forwardRef<FormHandles>((props, ref) => {
  const inputRef1 = useRef<HTMLInputElement>(null);
  const inputRef2 = useRef<HTMLInputElement>(null);

  useImperativeHandle(ref, () => ({
    inputRef1,
    inputRef2,
  }));

  return (
    <form>
      <input ref={inputRef1} />
      <input ref={inputRef2} />
    </form>
  );
});

const App: React.FC = () => {
  const formRef = useRef<FormHandles>(null);
  return <Form ref={formRef} />;
};
Enter fullscreen mode Exit fullscreen mode
  • Anonymous Functions in Dev Tools: Using arrow functions with forwardRef results in anonymous functions in Dev Tools unless you name the function twice:
const NamedComponent = forwardRef<HTMLDivElement>((props, ref) => (
  <div ref={ref}>Named Component</div>
));
Enter fullscreen mode Exit fullscreen mode
const NamedComponent = forwardRef<HTMLDivElement>(function NamedComponent(props, ref) {
  return <div ref={ref}>Named Component</div>;
});
Enter fullscreen mode Exit fullscreen mode
  • Extra Boilerplate: We need to use additional API and imports, making our code more complex and less readable.
  • Nested Components: Passing refs through multiple layers of components adds unnecessary complexity.
const InnerComponent = forwardRef<HTMLDivElement>((props, ref) => (
  <div ref={ref}>Inner Component</div>
));

const OuterComponent = forwardRef<HTMLDivElement>((props, ref) => (
  <InnerComponent ref={ref} />
));

const App: React.FC = () => {
  const outerRef = useRef<HTMLDivElement>(null);

  return <OuterComponent ref={outerRef} />;
};
Enter fullscreen mode Exit fullscreen mode
  • Non-Descriptive Prop Names: Generic ref names like ref are not descriptive, making it unclear where the ref is being attached.

  • Typing Issues with Generics: forwardRef breaks TypeScript generics, making type inference harder and less reliable. You can find more information here.

  • Potential Performance Issues: Wrapping components in forwardRef can slow down rendering, especially in stress tests with a large number of components. You can find more information here.

The Simpler Alternative

A simpler and better alternative exists: using custom ref props. Instead of ref, we can use any other prop name like firstInputRef. This pattern works automatically with functional components, solving all the issues mentioned:

import React, {Ref, useRef, useEffect } from 'react';

interface ChildProps {
  firstInputRef: Ref<HTMLInputElement>;
}

const Child: React.FC<ChildProps> = ({ firstInputRef }) => (
  <div>
    <input ref={firstInputRef} />
  </div>
);

const App: React.FC = () => {
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    if (inputRef.current) {
      console.log(inputRef.current); // <input />
    }
  }, []);

  return <Child firstInputRef={inputRef} />;
};
Enter fullscreen mode Exit fullscreen mode

Conclusion

For most cases, using custom ref props is a better solution than forwardRef. It simplifies our code, improves readability, and avoids many issues. forwardRef is only necessary in specific scenarios like single element proxy components or when simulating instance refs. With the new RFC potentially removing forwardRef, we can look forward to a simpler, more intuitive way of handling refs in React.

Image by u_vplf3ftkcz from Pixabay.

Was that helpful?

Top comments (0)