Why There’s No Need for forwardRef in React
Tags: react, webdev, javascript, typescript
Published: 2024-07-03 07:37:16

## 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](https://github.com/reactjs/rfcs/pull/107#issuecomment-466304382) 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.

```ts
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} />;
};
```
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:

```ts
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} />;
};
```
## 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:

```ts
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} />;
};
```
- **Anonymous Functions in Dev Tools:** Using arrow functions with `forwardRef` results in anonymous functions in Dev Tools unless you name the function twice:

```ts
const NamedComponent = forwardRef<HTMLDivElement>((props, ref) => (
  <div ref={ref}>Named Component</div>
));
```

```ts
const NamedComponent = forwardRef<HTMLDivElement>(function NamedComponent(props, ref) {
  return <div ref={ref}>Named Component</div>;
});
```
- **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.

```ts
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} />;
};
```
- **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](https://fettblog.eu/typescript-react-generic-forward-refs/).

- **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](https://github.com/facebook/react/issues/13456).

## 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:

```ts
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} />;
};
```
## 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](https://pixabay.com/users/u_vplf3ftkcz-41657635/) from [Pixabay](https://pixabay.com/).

Was that helpful?