selbekk

The Hitchhikers Guide to Refs

March 14, 2019
9 min read

Ever got confused about refs in React? This article is about to set the record straight.

React has this feature called refs. A ref is what the React docs call an "escape hatch", and lets you interact with instances of stuff. They should be used sparingly, but can be pretty useful at times.

This article will guide you through the sometimes confusing world of refs in React, and how, when and why you should use them. We'll go through what refs are, the different ways to create them, use them and when they're the right choice for your challenge. Let's go! 💥

What's a ref?

A ref - short for reference - is a way to reference... well, something. Typically that something is a DOM node or a class component. More precisely, a ref is a reference to the instance of a node or component.

In the case of a DOM node, you get access to its JavaScript API. Just like you'd created it in JavaScript yourself, like this:

const buttonRef = document.createElement('button');

This means you get access to a ton of imperative APIs, like .focus() or .click()

When you add a ref to a class component, you get access to its instance. That means you can call all its instance methods, if you need to. This might be useful if you need to fetch some state from a child component, or trigger some sort of side-effect that can't easily be triggered by passing a changed prop.

How do you create a ref?

Alright, so we've looked at what a ref is - but how do we use it? Turns out, there's a few ways. Let's dive in!

React.createRef()

The simplest way to create a ref is by using the API provided to us by React. By calling React.createRef(), we receive a ref we can place on whatever we want:

function App() {
  const inputRef = React.createRef();
  return (
    <>
      <input ref={inputRef} />
      <button onClick={() => inputRef.current.focus()}> Click to focus </button>
    </>
  );
}

Here, we put the reference on the input field by passing it to the ref prop, and then we call it from the button's click handler. If you click the button, the input field will be focused. Not something you do every day, perhaps, but you get the gist.

.current what?
When you call React.createRef(), you get back an object that has a single property, current. Why do we need this seemingly annoying middle-man - couldn't we just assign what we wanted directly?

Turns out, there's a reason for this! If we created the ref directly, there would be no way for React to update this ref - we'd have to create a new reference from scratch. By having this mutable object property, though, React can update it in place, without having to recreate it again. You can look at this issue for a better explanation.

React.createRef() is a pretty new addition to React (it was added in 16.3.0). It was added to simplify this entire refs process. Have a look at the RFC if you want to dive into why it was added in the first place.

useRef

useRef is a hook, and will therefore only work in function components. That doesn't mean that it's not a great tool! As a matter of fact, this is what I use to write 99 % of my refs these days.

When you call useRef you get an immutable object instance with a mutable current value, like React.createRef(). You can pass in an initial value if you'd like. Here's an example!

function App() {
  const inputRef = React.useRef(null);
  return (
    <>
      <input ref={inputRef} />
      <button onClick={() => inputRef.current.focus()}> Click to focus </button>
    </>
  );
}

Callback refs

Before React 16.3 came out, the preferred way of creating a ref was by something we called "callback refs". Basically it put you in charge of creating and storing the reference somewhere. It looked like this:

class App extends React.Component {
  render() {
    return (
      <>
        <input ref={(ref) => (this.inputRef = ref)} />
        <button onClick={() => this.inputRef.focus()}> Click to focus </button>
      </>
    );
  }
}

This works just fine too, but it was considered a bit clunky to understand for a few reasons. First of all, now we had to decide on where I'd like to save my ref. Second, there was a caveat of inlining the function itself. If the callback function was inlined (like above), it would be called twice - once with the ref argument being null, and once with it being set to the actual instance.

You can still use this approach today, but save it for when you need the flexibility it offers. If you're dynamically creating references, for example - that might be a use case for this.

String refs (vintage)

If you're working on an older codebase, you might stumble across code like this:

class App extends React.Component {
  render() {
    return (
      <>
        <input ref="input" />
        <button onClick={() => this.refs.input.focus()}>Click to focus</button>
      </>
    );
  }
}

This approach is called "string refs", and is scheduled for deprecation sometime in the future. The API is nice and simple, but there's a few drawbacks to them.

If you see them in your codebase, refactor them to look like the previous example instead, or use React.createRef() to create the ref for you. Your future self will thank you!

When to use refs (and when to avoid them)

As I mentioned in the introduction, React calls refs an "escape hatch". Why's that? To answer that, we need to remember that React is declarative.

Declarative vs imperative
I always forget what's what, so here's a quick rule of thumb for those of you who think like me:

Declarative code tells you what you want. Imperative code tells you how to get there.

React is declarative in the way that you write what you want (an H2 with a class of "large"), instead of specifying how that's going to happen. Declarative code rules!

In a perfect world, everything would be declarative - but the web isn't built that way. For good reason, I might add. Instead, we sometimes need to "escape" down into the imperative world. Some examples are:

  • focusing an input (yeah yeah, we've covered this one)
  • accessing imperative APIs from other libraries (i.e. jQuery)
  • accessing DOM APIs (speech recognition, animation, you name it)
  • invoking functionality on child components

For all of these use cases, we gain that imperative access by creating a reference to them. Once we have that, we can go to town, calling methods and being imperative all day long.

Refs should be the exception to the rule

Although there are some real usecases for refs - they aren't something you should be grabbing for every time you want something to happen in your apps. Typically, a small refactor, lifting some state up or creating a declarative abstraction over imperative APIs is what you want to do.

In other words - try solving your challenge without refs first. If you can't see a good solution, then consider a ref.

Forwarding refs

ref is a special prop in React. Like key, it's not passed along as a part of the props hash passed to the component. Instead, it's "captured" by React, and is never really exposed to the component being referenced.

Now, 9 times out of 10, this is what you want. If you're creating a reusable button or input component, however, the ref prop passed might have been meant for the actual DOM field.

Back in the days, you'd have to create a new prop (inputRef or domRef or what have you), and then apply that prop to your DOM node, like this:

function InputField(props) {
  return <input ref={props.inputRef} />;
}

This would lead to a lot of confusing APIs! Now, we have React.forwardRef:

React.forwardRef((props, ref) => {
  return <input ref={ref} {...props} />;
});

This will just forward whatever ref is sent to the correct place!

It's not an API you're going to use a lot - but it's a great thing to know it's there. You can read more about forwardRef in the documentation.

useRef - changing how we think about refs

Up until very recently, refs were about what we've talked about so far - references to DOM nodes or class component instances. However, with the introduction of hooks came useRef - and it changes everything. Again.

As you saw earlier, you can use useRef in a similar fashion to React.createRef in function components to create a reference. However, useRef doesn't limit itself to just instances of stuff!

As a matter of fact, useRef lends itself to any value, instance, function or whatever else you might want to keep around between renders. Think of it as the "instance variable" of function components.

Here's an example. I often create an InputGroup component in my apps to automatically create a UUID id for my input fields, like so:

import uuid from 'uuid/v4';
class InputGroup extends React.Component {
  id = `input-${uuid()}`;
  render() {
    return (
      <div>
        <label htmlFor={this.id}>{this.props.label}</label>
        {children({ id: this.id })}
      </div>
    );
  }
}

It's annoying to have to use a class component here - I'm not using any fancy React features! Let's refactor it to a function component instead:

import uuid from 'uuid/v4';
function InputGroup(props) {
  const id = useRef(uuid());
  return (
    <div>
      <label htmlFor={id}>{props.label}</label> {children({ id })}
    </div>
  );
}

This is pretty neat - I can now share values across calls to my function component! I suggest you go check out the official documentation on useRef - it has some nice examples to learn from.

This is kind of an aside to the topic of refs - but I included it for completeness sake. Since you can create DOM refs the same way - it's a nice trick to know about.

Conclusion

Refs are a great tool to have in your React toolbox. They're perfect for when you need to trigger some imperative DOM API, or you need to access to the instance of a class component. You should use refs sparingly, and only if you need to access imperative APIs for some reason. Consider lifting state up instead of referencing class component instances.

There are plenty of ways to create refs, but the easiest is React.useRef for function components, or for React.createRef for any component. You might stumble across callback refs or string refs in legacy codebases, but new code shouldn't use them without a good reason.

Finally, useRef lets us create refs to not only DOM nodes and component instances, but to any value, function or component.

Want more material?

Here's a few useful links to other content that describe the same as above, with other words and granularities:

All rights reserved © 2024