Jeremias Menichelli Curious mind, writer, and developer.

A complete guide to React refs

9 min read 2522

A complete guide to React refs

Editor’s note: This post was updated on 13 January 2022 to amend the tutorial to include functional React components, per their official recommendation, remove references to the createRef function, and include some other new information.

As is the case with many other UI libraries, React offers a way to rethink a view as the result of a state of a component. This is a big pivot away from how we usually build applications.

When we become familiar with some of these new concepts, we discover how easy it is to solve simple problems in the frontend world that used to cause us some trouble. Part of that benefit comes from creating views with the abstraction mechanisms that React and JSX expose, instead of doing it through DOM spec methods.

Still, the React team did something smart: they provided escape hatches and kept the library open for situations beyond the ones they were specifically designing for, as well as situations the model may not work for. We’ll cover anti-patterns later in this article.

These escape hatches are refs, which allow us to access DOM properties directly. Normally, React uses state to update the data on the screen by re-rendering the component for us. But there are certain situations where you need to deal with the DOM properties directly, and that’s where refs come in clutch.

An example of this would be auto-focusing a text box when a component renders. React doesn’t provide an easy way to do this, so we can use refs to access the DOM directly and focus the text box for us whenever the component renders on the screen.

In this article, we’re going to investigate why React, a framework meant to abstract your code away from DOM manipulation, leaves the door open for developers to access it.

Creating refs

When working with class-based components in the past, we used createRef() to create a ref. However, now that React recommends functional components, and general practice is to follow the Hooks way of doing things, we don’t need to use createRef(). Instead, we use useRef(null) to create refs in functional components.

As stated in the intro, refs are escape hatches for React developers, and we should try to avoid using them if possible.

When we obtain a node using a ref and later modify some attribute or the DOM structure of it, it can enter into conflict with React’s diff and update approaches.

We made a custom demo for .
No really. Click here to check it out.

First, let’s start with a simple component and grab a node element using refs.

import React from "react";
const ActionButton = ({ label, action }) => {
  return <button onClick={action}>{label}</button>;
};
export default ActionButton;

The <button> expression here is actually the JSX way of calling the React.createElement('button') statement, which is not actually a representation of an HTML button element — it’s a React element.

You can gain access to the actual HTML element by creating a React reference and passing it to the element itself.

import React, { useRef } from "react";
const ActionButton = ({ label, action }) => {
  const buttonRef = useRef(null);
  return (
    <button onClick={action} ref={buttonRef}>
      {label}
    </button>
  );
};
export default ActionButton;

This way, at any time in the lifecycle of the component, we can access the actual HTML element at buttonRef.current.

Now we know how to access DOM nodes inside a React component. Let’s take a look at some of the situations where this may be useful.

Using React refs

One of the many concepts that React popularized among developers is the concept of declarative views. Before declarative views, most of us were modifying the DOM by calling functions that explicitly changed it.

As mentioned at the introduction of this article, we are now declaring views based on a state, and — though we are still calling functions to alter this state — we are not in control of when the DOM will change or even if it should change.

Because of this inversion of control, we’d lose this imperative nature if it weren’t for refs.
Here are a few use cases where it may make sense to bring refs into your code.

Focus control

You can achieve focus in an element programmatically by calling focus() on the node instance.
Because the DOM exposes this as a function call, the best way to do this in React is to create a ref and manually do it when we think it’s suitable.

import React, { useState } from "react";
const InputModal = ({ initialValue, onSubmit, onClose }) => {
  const [value, setValue] = useState(initialValue);
  const onChange = (e) => {
    setValue(e.target.value);
  };
  const onSubmit = (e) => {
    e.preventDefault();
    onSubmit(value);
    onClose();
  };
  return (
    <div className="modal--overlay">
      <div className="modal">
        <h1>Insert a new value</h1>
        <form action="?" onSubmit={onSubmit}>
          <input type="text" onChange={onChange} value={value} />
          <button>Save new value</button>
        </form>
      </div>
    </div>
  );
};
export default InputModal;

In this modal, we allow the user to modify a value already set in the screen below. It would be a better user experience if the input was on focus when the modal opens, which could enable a smooth keyboard transition between the two screens.

The first thing we need to do is get a reference for the input:

import React, { useRef, useState } from "react";
const InputModal = ({ initialValue, onSubmit, onClose }) => {
  const [value, setValue] = useState(initialValue);
  const inputRef = useRef(null);
  const onChange = (e) => {
    setValue(e.target.value);
  };
  const onSubmit = (e) => {
    e.preventDefault();
    onSubmit(value);
    onClose();
  };
  return (
    <div className="modal--overlay">
      <div className="modal">
        <h1>Insert a new value</h1>
        <form action="?" onSubmit={onSubmit}>
          <input ref={inputRef} type="text" onChange={onChange} value={value} />
          <button>Save new value</button>
        </form>
      </div>
    </div>
  );
};
export default InputModal;

Next, when our modal mounts, we imperatively call focus on our input ref within a useEffect:

import React, { useEffect, useRef, useState } from "react";
const InputModal = ({ initialValue, onSubmit, onClose }) => {
  const [value, setValue] = useState(initialValue);
  const inputRef = useRef(null);
  useEffect(() => {
    inputRef.current.focus();
  }, [])
  const onChange = (e) => {
    setValue(e.target.value);
  };
  const onSubmit = (e) => {
    e.preventDefault();
    onSubmit(value);
    onClose();
  };
  return (
    <div className="modal--overlay">
      <div className="modal">
        <h1>Insert a new value</h1>
        <form action="?" onSubmit={onSubmit}>
          <input ref={inputRef} type="text" onChange={onChange} value={value} />
          <button>Save new value</button>
        </form>
      </div>
    </div>
  );
};
export default InputModal;

So when you open the modal, you should see the text box focused by default:

When you open the modal, the text box is automatically focused

Remember that you need to access the element through the current property.

Detect if an element is contained

Similarly, sometimes you want to know if any element dispatching an event should trigger some action on your app. For example, our modal component could be closed if the user clicked outside of it:

import React, { useEffect, useRef, useState } from "react";
const InputModal = ({ initialValue, onClose, onSubmit }) => {
  const [value, setValue] = useState(initialValue);
  const inputRef = useRef(null);
  const modalRef = useRef(null);
  useEffect(() => {
    inputRef.current.focus();
    document.body.addEventListener("click", onClickOutside);
    return () => document.removeEventListener("click", onClickOutside);
  }, []);
  const onClickOutside = (e) => {
    const element = e.target;
    if (modalRef.current && !modalRef.current.contains(element)) {
      e.preventDefault();
      e.stopPropagation();
      onClose();
    }
  };
  const onChange = (e) => {
    setValue(e.target.value);
  };
  const onSub = (e) => {
    e.preventDefault();
    onSubmit(value);
    onClose();
  };
  return (
    <div className="modal--overlay">
      <div className="modal" ref={modalRef}>
        <h1>Insert a new value</h1>
        <form action="?" onSubmit={onSub}>
          <input ref={inputRef} type="text" onChange={onChange} value={value} />
          <button>Save new value</button>
        </form>
      </div>
    </div>
  );
};
export default InputModal;

Here, we are checking if the element click is outside the modal limits. It will work like this:

Check to see if the element click is outside the modal limits

If it is, then we are preventing further actions and calling the onClose callback, since the modal component expects to be controlled by its parent.

Remember to check if the DOM element’s current reference still exists, as state changes in React are asynchronous.

To achieve this, we are adding a global click listener on the body element. It’s important to remember to clean the listener when the element gets unmounted.

Integrating with DOM-based libraries

As good as React is, there are a lot of utilities and libraries outside its ecosystem that have been in use on the web for years. For example, using refs allows us to combine React with a great animation library. It’s good to take advantage of their stability and resolution for some specific problems.

The GSAP library is a popular choice for animation examples. To use it, we need to send a DOM element to any of its methods.

Let’s go back to our modal and add some animations to make its appearance fancier.

import React, { useEffect, useRef, useState } from "react";
import gsap from "gsap";
const InputModal = ({ initialValue, onClose, onSubmit }) => {
  const [value, setValue] = useState(initialValue);
  const inputRef = useRef(null);
  const modalRef = useRef(null);
  const overlayRef = useRef(null);
  const onComplete = () => {
    inputRef.current.focus();
  };
  const timeline = gsap.timeline({ paused: true, onComplete });
  useEffect(() => {
    timeline
      .from(overlayRef.current, {
        duration: 0.25,
        autoAlpha: 0,
      })
      .from(modalRef.current, {
        duration: 0.25,
        autoAlpha: 0,
        y: 25,
      });
    timeline.play();
    document.body.addEventListener("click", onClickOutside);
    return () => {
      timeline.kill();
      document.removeEventListener("click", onClickOutside);
    };
  }, []);
  const onClickOutside = (e) => {
    const element = e.target;
    if (modalRef.current && !modalRef.current.contains(element)) {
      e.preventDefault();
      e.stopPropagation();
      onClose();
    }
  };
  const onChange = (e) => {
    setValue(e.target.value);
  };
  const onSub = (e) => {
    e.preventDefault();
    onSubmit(value);
    onClose();
  };
  return (
    <div className="modal--overlay" ref={overlayRef}>
      <div className="modal" ref={modalRef}>
        <h1>Insert a new value</h1>
        <form action="?" onSubmit={onSub}>
          <input ref={inputRef} type="text" onChange={onChange} value={value} />
          <button>Save new value</button>
        </form>
      </div>
    </div>
  );
};
export default InputModal;

Add animations to our modal to make it look fancy!

At the constructor level, we are setting up the initial animation values, which will modify the styles of our DOM references. The timeline only plays when the component mounts.

When the element gets unmounted, we’ll clean the DOM state and actions by terminating any ongoing animation with the kill() method supplied by the Timeline instance.

We’ll turn our focus to the input after the timeline has completed.

Avoiding React ref anti-patterns

Once you know how refs work, it’s easy to use them where they’re not needed. There’s more than one way to achieve the same thing inside a React component, so it’s easy to fall into an anti-pattern.

My rule when it comes to ref usage is this: only use a ref when you need to imperatively call a function for a behavior React doesn’t allow you to control.

A simpler way to put it would be: when you need to call a function, and that function has no association with a React method or artifact, use a ref.

Let’s explore an anti-pattern that I’ve seen repeatedly.

import React, { useRef } from "react";
const Form = () => {
  const [storedValue, setStoredValue] = useState("");
  const inputRef = useRef(null);
  const onSubmit = (e) => {
    e.preventDefault();
    setStoredValue(inputRef.current.value);
  };
  return (
    <div className="modal">
      <form action="?" onSubmit={onSubmit}>
        <input ref={inputRef} type="text" />
        <button>Submit</button>
      </form>
    </div>
  );
};

It’s fair to say if you want to send a value on submit, this approach will work, but the issue here is that because we know refs provide an escape hatch of the view model React offers, we can too easily go sniffing into DOM element values or properties that we can access through React’s interface.

Controlling the input value means we can always check its value. We don’t need to use refs here to access the value of the text box. We can use the value provided by React itself:

return (
  <input
    type="text"
    onChange={e => setValue(e.target.value)}
    value={value}
  />
)

Let’s go back to our rule: “Only use a ref when you need to imperatively call a function for a behavior React doesn’t allow you to control. ”

In our uncontrolled input, we create a ref but we don’t make an imperative call. Then that function should exist, which is not satisfied as I can indeed control an input’s value.

Using forwardRef

As we’ve discussed, refs are actually useful for really specific actions. The examples shown are a little simpler than what we usually find in a web application codebase nowadays. Components are more complex, and we barely use plain HTML elements directly. It’s really common to include more than one node to encapsulate more logic around the view behavior.

import React from 'react'

const LabelledInput = (props) => {
  const { id, label, value, onChange } = props

  return (
    <div class="labelled--input">
      <label for={id}>{label}</label>
      <input id={id} onChange={onChange} value={value} />
    </div>
  )
}

export default LabelledInput

The issue now is that passing a ref to this component will return its instance, a React component reference, and not the input element we want to focus on, like in our first example.

Luckily, React provides an out-of-the-box solution for this called forwardRef, which allows you to define internally what element the ref will point at.

import React from 'react'

const LabelledInput = (props, ref) => {
  const { id, label, value, onChange } = props

  return (
    <div class="labelled--input">
      <label for={id}>{label}</label>
      <input id={id} onChange={onChange} value={value} ref={ref}/>
    </div>
  )
}

export default React.forwardRef(LabelledInput)

See this example in action:

To achieve this, we’ll pass a second argument to our function and place it in the desired element.
Now, when a parent component passes a ref value, it’s going to obtain the input, which is helpful to avoid exposing the internals and properties of a component and breaking its encapsulation.

The example of our form that we saw failing to achieve focus will now work as expected.

Conclusion

We started with a recap on the basic concepts of React and its usage, why we generally shouldn’t break the framework’s model, and why we may sometimes need to.

Accessing the DOM through the interface the library exposes helps to maintain the internals of React in place (remember that useState contains more logic than just triggering a re-render cycle, like batching updates and, in the future, time slicing).

Breaking this model with anti-patterns can render later performance improvements in the library useless, or even create bugs in your applications.

Remember to use refs only when there is an implicit function call that React can’t handle through its methods. Also, make sure it doesn’t alter the internal state of the components. For more information, read the official React documentation about refs.

Jeremias Menichelli Curious mind, writer, and developer.

6 Replies to “A complete guide to React refs”

  1. Thanks for this article, Jeremias. I use `useRef` and `createRef` extensively with my React code, which is entirely function, not class based, and extensively using Hooks. I’ve examined some `forwardRef` code and found it insanely complex and confusing so just used `window.###` functions instead.

  2. Little typo in the last sample, should be

    export default React.forwardRef(LabelledInput)

Leave a Reply