The Complete Guide to React Refs

June 17, 201911 min read

Last reviewed June 17, 2019

This article was originally posted in the Modus Create blog.

The React API is fairly simple, even though it has been growing bigger. With recent features such as Context and Hooks, the whole ecosystem has become more complete. Yet, some concepts are usually a source of struggle for beginners and even experienced developers. One of them is the Refs API.

Short for “reference”, refs are a way to access underlying DOM elements in a React component. There are many reasons why you would want to access the DOM. Common use-cases are managing focus (critical for accessibility) and triggering animations. In this post, you will learn how to properly use refs, how to use the current API, and decide when to approach one over the other.

The Problem with Refs

Manipulating DOM elements is JavaScript 101. So why is it that devs don’t feel comfortable using refs?

Imperative Paradigm

Refs are basically imperative. This contrasts with the declarative nature of React. To illustrate this, consider the following comparison between attaching an event handler to old-school JavaScript and modern React:

<!-- HTML -->
<div id="my-custom-button">Click me!</div>
// Vanilla JavaScript
var onClick = function() { console.log("Clicked!"); }
var button = document.getElementById("my-custom-button");
button.addEventListener(‘click’, onClick);

In conventional JavaScript, you would tell exactly how you want your code to proceed with the onClick handler. You would select the element and then attach the handler to it using the JavaScript API.

If you were to implement this in React, your code would be something like this:

// React
const customButton = () => {
  const onClick = () => console.log("Clicked!");
  return <div onClick={onClick}>Click me!</div>
};

Aside from the obvious syntax sugar, notice that we declare what we expect to happen on click, without bothering too much on how we do it. This is the basic difference between those two approaches. Now, with refs you proceed very much like the first example: you define how to control the element. More details in a bit…

Multiple APIs

When refs were first born, the React team encouraged the use of string refs. This is no longer the case as this API will be deprecated. A powerful alternative was introduced: callback refs. But all this power came with a price – callback refs are more verbose and may behave oddly. In order to simplify things, the createRef API came into play. And finally, after Hooks were introduced, useRef emerged. But, because there are four ways of doing the same thing, people started losing faith in refs. Let’s fix this.

Deciding Between Callback Refs and createRef

With string refs condemned to the antique section of the React museum, a vital question remains: should we use callback refs or the createRef API?

The short answer is that most of the time you can safely use the createRef API. Although you can always achieve the same result using callback refs, recall that this new API was specially crafted in order to simplify your experience. You can look at its RFC in order to understand the React team’s motivations behind it. In short, the goal was to maintain the simplicity of the deprecated string refs and purposely keep a simple API, leaving callback refs for more complex use cases.

In order to clarify the examples included in this post, I created a simple cheat sheet that you can check anytime during your reading. The code is available in GitHub.

Consider then the common case when you want to programmatically trigger focus on an element:

class SimpleRef extends Component {

  constructor() {
    super();
    this.inputRef = React.createRef();
  }

  onClick() {
    this.inputRef.current.focus();
  }

  render() {
    return (
      <div>
        <input ref={this.inputRef} />
        <button onClick={this.onClick.bind(this)}>Click to Focus</button>
      </div>
    );
  }
}

The API is very simple. You first define a ref, assign it to the element you want to manipulate and call focus on ref’s current property.

This is how to achieve the same using callback refs:

class SimpleCallbackRef extends Component {

  onClick() {
    this.inputRef.focus();
  }

  render() {
    return (
      <div>
        <input ref={ref => { this.inputRef = ref; }} />
        <button onClick={this.onClick.bind(this)}>Click to Focus</button>
      </div>
    );
  }
}

Notice that although you don’t need to manually create a ref anymore, the callback function ref => { this.inputRef = ref; } looks less natural. But there’s also an annoying caveat. Consider the following example:

class InlineCallbackRefWithReRender extends Component {

  constructor() {
    super();
    this.state = { count: 0 };
  }

  onClick() {
    this.inputRef.focus();
    this.setState({count: this.state.count + 1});
  }

  render() {
    return (
      <div>
        <input ref={ref => { this.inputRef = ref; }} />
        <button onClick={this.onClick.bind(this)}>Click to Focus</button>
      </div>
    );
  }
}

Now, we also trigger a re-render when the state changes. This has the peculiarity of calling the callback twice: first time with null and then with the correct value. The docs also reserve a section to explain this.

It means that the following would raise an error (ref is null during the first call):

<input ref={ref => ref.focus() } />

You can fix this by adding a safe condition ref => ref && ref.focus() or by binding the callback to a class method in the constructor. However, it won’t help if you bind directly in the render function.

class ConstructorBoundCallbackRefWithReRender extends Component {

  constructor() {
    super();
    this.state = { count: 0 };
    // Need to bind in the constructor
    this.onRefMount = this.onRefMount.bind(this);
  }

  onClick() {
    this.inputRef.focus();
    this.setState({count: this.state.count + 1})
  }

  onRefMount(ref) {
    // Good! Called only once, during mounting
    ref.focus();
    this.inputRef = ref;
  }

  render() {
    return (
      <div>
        <input ref={this.onRefMount} />
        <button onClick={this.onClick.bind(this)}>Click to Focus</button>
      </div>
    );
  }
}

Notice how close to the createRef API it has become. Also, you are probably disappointed with callback refs already. They are too fragile. There are too many ways to breaking things and there are too many things to try and remember correctly.

Drama aside, it is clear why the React team has favored the createRef API for simple cases, which should be enough most of the times. Since I’m a fan of APIs which require me to think less, and therefore are less error prone, createRef is the winner here.

Function Components

In order to simplify things even more, consider writing a function component:

const FunctionComponentWithRef = () => {
  const textInput = React.createRef();
  return (
    <div>
      <input ref={textInput} />
      <button onClick={() => textInput.current.focus()}>
        Click to Focus
      </button>
    </div>
  );
};

Which works fine in this simple case but is limited once function components can’t do everything a class does. Fortunately, with Hooks becoming official since React 16.8.0, this has changed. More about Hooks later.

The Case for Callback Refs

Sure, createRef provides a simple API. Still, callback refs weren’t deprecated. What’s good about them?

Consider the case when you need to create refs dynamically. Using createRef you first create and then assign the reference. This may put you in trouble when your ref does not share the same lifecycle as the parent.

Imagine a situation where the user can create a dynamic li