Peter Ekene Eze Learn, Apply, Share

Cleaning up the DOM with ForwardRef in React

6 min read 1919

Ref forwarding in React is a feature that lets components pass down (“forward”) refs to their children. It gives the child component a reference to a DOM element created by its parent component. This then allows the child to read and modify that element anywhere it is being used.

In this tutorial, we will go over the concept of forwarding refs in React and understand how it helps us manage interactions with the DOM. For a more engaging experience, we’ll cover how to create refs, attach created refs to DOM elements and classes, forward refs, and so on.

It is also worthy to note that we will often make references to the docs page to build on the information that already exists and prove our concepts with relatable real-life examples and snippets to be hosted on CodeSandbox.

What is ref forwarding?

To understand Ref forwarding, we must first understand what refs are, how they work, and go over a few use cases. Typically in React, parent components pass data down to their children via props.

To change the behavior of a child component you render it with a new set of props. To modify a child component such that it exhibits a slightly different behavior, we need a way to make this change without reaching for the state or re-rendering the component.

We can achieve this by using refs. With refs, we have access to a DOM node that is represented by an element. As a result, we can modify it without touching its state or re-rendering it.

Since refs hold a reference to the DOM element itself, we can manipulate it with native JavaScript functions that are unavailable in the React library. For instance, we can initiate focus on input field when a button is clicked:

import ReactDOM from "react-dom";
import React, { Component } from "react";
export default class App extends Component {
  constructor(props) {
    super(props);
    this.myInput = React.createRef();
  }
  render() {
    return (
      <div>
        <input ref={this.myInput} />
        <button
          onClick={() => {
            this.myInput.current.focus();
          }}
        >
          focus!
        </button>
      </div>
    );
  }
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

You can also find the code at CodeSandbox.

To implement this with pure JavaScript we could do something like this:

document.getElementById('input').focus

Through the ref, we manipulated our cursor to automatically focus on the input element whenever the button is clicked. Without refs, we’d have to use state to check if the input field should focus or not — that is before making a decision, which is often unnecessary in cases like this.

When to use refs

As seen in the official React documentation, there are a few good use cases for refs:

Managing focus, text selection, or media playback

Let’s imagine you have an input component. In some parts of your application, you may want the cursor focused on it when a user clicks a button. It makes more sense to modify only that particular instance of the input component without changing the state (via refs), rather than changing the state (via props) which will cause the component to re-render every-time. Similarly, we can use refs to control the state of music or video players (pause, play, stop) without them re-rendering anytime we click a button (change the state).

Incrementing values

Think about a Medium clap button. A quick way to implement a similar feature would be to increment the count value stored in the state every time a user clicks a clap. However, this may not be very efficient. Every time a user clicks the clap button it will re-render, and if we are sending a network request to store the value in a server it will get sent as many times as the button is clicked. With refs, we can target that particular node and increment it every time a user clicks the button without causing a re-render and finally, we can send one request to our server with the final value.

Triggering imperative animations

We can use refs to trigger animation between elements that rely on themselves for their next state but exist in different components (this concept is called ref forwarding). Refs can also be used to simplify integration with third-party DOM libraries and managing multistep form value states etc.

Creating refs

To create a ref, React provides a function called React.createRef(). Once created they can be attached to React elements via the ref attribute. It is also worthy to note that refs are somewhat similar to state. When a component is constructed, refs get assigned to instance properties of that component ensuring that they can be referenced anywhere in the component:

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.newRef = React.createRef(); //newRef is now available for use throughout our component
  }
 ...
}

At this point, we have created a Ref called newRef. To use this Ref in our component, we simply pass it as a value to the ref attribute like this:

class MyComponent extends React.Component {
 ...
  render() {
    return <div ref={this.myRef} />;
  }
}

We’ve attached the Ref here and passed in the newRef as it’s value. As a result, we now have the ability to update this without changing state.

Attaching refs

Refs are created when a component is rendered and can be defined either in the componentDidMount() or in the constructor(). As such, they can be attached to DOM elements or class components but cannot be attached to function components because they don’t have instances.

Every Ref you define will represent a node in the DOM. Hence, when you want to reference that node in a render() function, React provides a current attribute that references the said node.

const DOMNode = this.newRef.current; // refers to the node it represents

The value of the ref differs depending on the type of the node it references (class components or DOM elements).

For a better understanding of refs and type of node they reference, and the default values associated with each, let’s consider this piece from the documentation:

  • When the ref attribute is used on an HTML element, the ref created in the constructor with React.createRef() receives the underlying DOM element as its current property
  • When the ref attribute is used on a custom class component, the ref object receives the mounted instance of the component as its current i.e the components props, state, and methods

Let’s demonstrate this concept with a small video player. The video player will have some pause and play functionalities. To build along, create a new CodeSandbox project and add the following code:

import ReactDOM from "react-dom";
import React, { Component } from "react";

export default class App extends Component {
  constructor(props) {
    super(props);
    this.myVideo = React.createRef();
  }
  render() {
    return (
      <div>
        <video ref={this.myVideo} width="320" height="176" controls>
          <source
            src="https://res.cloudinary.com/daintu6ky/video/upload/v1573070866/Screen_Recording_2019-11-06_at_4.14.52_PM.mp4"
            type="video/mp4"
          />
        </video>
        <div>
          <button
            onClick={() => {
              this.myVideo.current.play();
            }}
          >
            Play
          </button>
          <button
            onClick={() => {
              this.myVideo.current.pause();
            }}
          >
            Pause
          </button>
        </div>
      </div>
    );
  }
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

You can also find the code here.

Here, we used ref to pause and play our video player by calling the pause and play methods on the video. When the pause or play button is clicked, the function will be called on the video player without a re-render.

Refs with function components

Refs cannot be attached to function components. Although, we can define refs and attach them to either DOM elements or class components. The bottom line is — function components do not have instances so you can’t reference them.

However, if you must attach a ref to a function component, the official React team recommends that you convert the component to a class, just like you would do when you need lifecycle methods or state.

Conditional refs

Aside from passing the default ref attribute, we can also pass functions to set refs. The major advantage of this approach is that you have more control over when refs are set and unset. That is possible because it gives us the ability to determine the state of the ref before certain actions are fired. Consider this snippet from the documentation page below:

class CustomTextInput extends React.Component {
  constructor(props) {
    super(props);
    this.textInput = null;
    this.setTextInputRef = element => {
      this.textInput = element;
    };
    this.focusTextInput = () => {
      // Focus the text input using the raw DOM API
      if (this.textInput) this.textInput.focus();
    };
  }
  componentDidMount() {
    this.focusTextInput();
  }
  render() {
    return (
      <div>
        <input
          type="text"
          ref={this.setTextInputRef}
        />
        <input
          type="button"
          value="Focus the text input"
          onClick={this.focusTextInput}
        />
      </div>
    );
  }
}

Instead of defining the refs in the constructor, we set the initial value to be null. The benefit of this approach is that textInput will not reference a node until the component is loaded (when the element is created).

Forwarding refs

When a child component needs to reference its parent components current node, the parent component needs a way to send down its ref to the child. The technique is called ref forwarding.

Ref forwarding is a technique for automatically passing a ref through a component to one of its children. It’s very useful when building reusable component libraries. forwardRef is a function used to pass the ref to a child component:

function SampleButton(props) {
  return (
    <button className="button">
      {props.children}
    </button>
  );
}

The SampleButton() component will tend to be used throughout the application in a similar manner as a regular DOM button, therefore accessing its DOM node may be unavoidable for managing focus, selection, or animations related to it.

In the example below, SampleComponent() uses React.forwardRef to obtain the ref passed to it, and then forward it to the DOM button that it renders:

const SampleButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="button">
    {props.children}
  </button>
));

const ref = React.createRef();
<SampleButton ref={ref}>Click me!</SampleButton>;

Now that we’ve wrapped the SampleButton component with the forwardRef method, components using it can get a ref to the underlying button DOM node and access it if necessary —just like if they used a DOM button directly.

Here’s a clarification for the code above:

  • We define a ref in the component that needs the ref and pass it to the button component
  • React will pass the ref through and forward it down to <button ref={ref}> by specifying it as a JSX attribute
  • When the ref is attached, ref.current will point to the <button> DOM node

The second ref argument in the SampleButton component only exists when you define a component with React.forwardRef call

Regular function or class components don’t receive the ref argument, and ref is not available in props either

Ref forwarding is not limited to DOM components. You can forward refs to class component instances, too

Conclusion

Using refs will definitely make our React code better as we will be more decisive in how we manage our state, props, and re-rendering. In this tutorial, we’ve covered the basics of refs and ref forwarding. We also looked at a few use cases and a few ways we can call refs. To read more about refs check out the docs here.

 

 

 

 

Plug: , a DVR for web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Peter Ekene Eze Learn, Apply, Share

Leave a Reply