React forwardRef example and why it should not be part of React API

React 16 has a forwardRef function. The function creates a new component with its ref attached to one of the child component. Why is this a big deal? In React, key and ref are special props which are not part of this.props within the component. I will explain this with a little bit of code.

Consider we have a component for entering SSN. The SSN component has three text boxes. 

SSN

The code for SSN component is as follows:

export default function SSN() {
  return (
    <>
      <input
        type="text"
        className="ssn1"
        placeholder="ssn"
      />
      <input type="text" className="ssn2" />
      <input type="text" className="ssn3" />
    </>
  );
}

Passing ref to the child component

If SSN component is the first component with a form, then we like to focus the first text box in SSN component. 

import React, { Component } from "react";
import SSN from "./SSN";

export default class Form extends Component {
  constructor() {
    super();
    this.ssnRef = React.createRef();
    this.state = {};
  }

  componentDidMount() {
    this.ssnRef.current.focus();
  }

  handleSubmit(e) {
    e.preventDefault();
    this.setState({ loading: true });
    setTimeout(() => {
      this.setState({ loading: false });
      this.ssnRef.current.focus();
    }, 3000);
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit.bind(this)}>
        <div>
          <SSN ref={this.ssnRef} />
        </div>
        <div>
          <input type="text" placeholder="first name" name="firstName" />
        </div>
        <div>
          <input type="text" name="lastName" placeholder="last name" />
        </div>
        <div>
          <button>{this.state.loading ? "Saving ..." : "Submit"}</button>
        </div>
      </form>
    );
  }
}

In the above form, we pass a ref to SSN using the ref prop. We use the ref to set focus when the form mounts on the DOM as well as after form submit event. However, this won’t work. Because within SSN component, this.props.ref is not available. Because ref is a special prop.

In the past, we overcame this limitation by using another prop like ssnRef. So, instead of 

<SSN ref={this.ssnRef} />

we used

<SSN ssnRef={this.ssnRef} />

And we attach ssnRef prop to the first text box within the SSN component to achieve the desired effect.

<input
  type="text"
  className="ssn1"
  placeholder="ssn"
  ref={props.ssnRef}
/>

The form with focus on SSN component is shown below.

Focus on SSN Text Box

Passing ref using forwardRef function

With React forwardRef API, we have support for passing the ref prop to a child component. In this case, the SSN component will pass the ref it receives to the first text box.

const SSN2 = React.forwardRef((props, ref) => (
  <>
    <input
      type="text"
      className="ssn1"
      placeholder="ssn - forwardRef"
      ref={ref}
    />
    <input type="text" className="ssn2" />
    <input type="text" className="ssn3" />
  </>
));

export default SSN2;

The forwardRef API provides the ref as the second argument to write a function component. There is really no magic involved. With this, we can pass the ref to SSN2 component as follows.

<SSN2 ref={this.ssnRef} />

This is how the React component tree looks.

ForwardRef wrapper component

We can give a display name to the forwardRef wrapper component.

SSN2.displayName = "SSN2";

With this change, our component tree looks like so.

Wrapper component with display name

forwardRef API and class Component

In this case, our SSN component is a function component. But consider our SSN component to be a class component. When SSN component is a class component, the corresponding forwardRef call looks like so.

class SSN3 extends Component {
  render() {
    return (
      <>
        <input
          type="text"
          className="ssn1"
          placeholder="ssn withClass"
          ref={this.props.ssnRef}
        />
        <input type="text" className="ssn2" />
        <input type="text" className="ssn3" />
      </>
    );
  }
}

export default React.forwardRef((props, ref) => 
  <SSN3 ssnRef={ref} />
);

We resort to passing a ssnRef (some other prop) to the class component. And use that other prop to attach the ref to the first text box.

The React component tree has additional wrapper component, an undesirable side-effect.

Additional ForwardRef wrapper over SSN3

Understandably, this undesirable side-effect will pass when we write function components with the proposed Hooks API. However, I have some more points, mostly on adding the forwardRef function to the React API.

CodePen for forwardRef example

Why we don’t need this API?

We already have a work-around for passing ref to child components. The work-around is to pass the ref as some other prop. In this case, we used ssnRef prop to pass the ref to a child component.

Why should the React team consider adding this API? It is for third-party package developers. There are several problems with this approach. 

When we pass the ref like this, we really don’t know that it is being forwarded. By providing an explicit alternate prop, we are clear that the ref is going to be forwarded.

Personally, I don’t see a reason to use a third-party package for creating a SSN component. As good React developers, we should also avoid packages like antd or bootstrap which create very standard and boring UI experiences. (Of course, unless we have time or budget constraints). So, having an API like forwardRef solely for package developers (which we should avoid) does not make sense.

Then there is this philosophical issue of bloated NPM packages as well as bloated product features. Over time, all products are bloated with features we don’t need. By yielding to a small minority of developers who need this feature, React team has bloated the product with code it has to maintain.

This is not the only feature that the React team has added for package developers. React Portal, the ability to insert a React component into some other part of the DOM, also suffers from a similar weakness. Traditionally, we solved the Portal problem by global state using Redux or Mobx. Instead of sending the entire component to another part of the tree, we send the component state to the global store. And the other component picks it up from the global store and refreshes itself. So, Portal is another feature we did not need.

My personal opinion is that we don’t need features like forwardRef or Portal which are just fashionable ways to do things in the React API.

Related Posts

6 thoughts on “React forwardRef example and why it should not be part of React API

  1. I disagree with you in the regard that ForwardRef is essential to pure functional components, as you don’t use classes to program, there would have no way to pass the ref.

    1. I just tried this in functional components, and it didn’t work. Do you mean that you have to use class syntax for the old ‘work-around’ to work properly?

  2. Thank you! This is the fourth article I’ve read trying to understand forwardRef API – now I understand how to use the work-around by sending it as props first.

  3. I mainly disagree with the point that forwardRef is “solely for package developers”.
    forwardRef provides a logical and straightforward way of accessing DOM nodes, when needed.
    The suggested workaround of creating a separate prop to pass down a ref seems counterintuitive to me: it adds an unnecessary prop to your component’s API and discourages use of the provided default way of passing DOM references to components.

  4. I disagree that we should avoid React packages. There are so many useful, well-tested React component packages out there that I trust. You should seriously consider _not_ reinventing every single wheel you want to put in your application.

Leave a Reply to Nicole Cancel reply

Your email address will not be published.