Jeremias Menichelli Curious mind, writer, and developer.

A complete guide to React refs

9 min read 2770

A Complete Guide to React Refs

Editor’s note: This post was updated on 23 February 2023 to add interactive code examples, update any outdated information, and include use cases for React refs. Check out our React Hooks cheat sheet to learn more about React Hooks.

As with many other UI libraries, React offers a way to rethink a view as the result of a component’s state. This is a significant pivot away from how we usually build applications. When we become familiar with some of these 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 clever: they provided escape hatches. They kept the library open for situations beyond the ones they were specifically designing for and 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 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 whenever the component renders on the screen.

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

Jump ahead:

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.

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 the JSX way of calling the React.createElement('button') statement, which is not 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 component’s lifecycle, 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.

Differences between useRef and createRef

The first difference between useRef and createRef is that createRef is typically used when creating a ref in a class component while useRef is used in function components. Additionally, createRef returns a new ref object each time it is called while useRef returns the same ref object on every render.

Another main difference is that createRef doesn’t accept an initial value, so the current property of the ref will be initially set to null. On the other hand, useRef can accept an initial value and the current property of the ref will be set to that value.

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 modified the DOM by calling functions that explicitly changed it. As mentioned in 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 bringing refs into your code may make sense.



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, as shown below:

import React, { useState } from "react";
const InputModal = ({ initialValue }) => {
  const [value, setValue] = useState(initialValue);
  const onChange = (e) => {
    setValue(e.target.value);
  };
  const onSubmit = (e) => {
    e.preventDefault();
  };
  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 UX if the input was on focus when the modal opens, which would 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 }) => {
  const [value, setValue] = useState(initialValue);
  const inputRef = useRef(null);
  const onChange = (e) => {
    setValue(e.target.value);
  };
  const onSubmit = (e) => {
    e.preventDefault();
  };
  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 }) => {
  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();
  };
  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:

Opening a Modal Focuses the Text Box

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

Here’s a CodeSandbox for the example above:

Detect if an element is contained

Similarly, 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, like so:

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 check if the element click is outside the modal limits. It will work like this:

Checking the Element Click in React

If it is, then we are preventing further actions and calling the onClose callback because 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 is unmounted.

Integrating with DOM-based libraries

As good as React is, many utilities and libraries outside its ecosystem 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;

Adding Animations to Our React Modal

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 is unmounted, we’ll clean the DOM state and actions by terminating any ongoing animation with the kill() method supplied by the Timeline instance.

Now, we’ll turn our focus to the input after the timeline has been completed.

When should you use useRef Hook?

There are some instances where you would want to use the useRef Hook, including the following:

  • Accessing DOM elements: You can use the useRef Hook when you need to interact with a specific DOM element in your component, such as setting the focus on an input field or measuring an element’s size
  • Storing values that don’t trigger re-renders: When you have a value that changes frequently but doesn’t trigger a re-render, you can use useRef to store that value. For example, if you have a timer in your component, you could use useRef to store the current time without triggering a re-render
  • Caching expensive computations: If you need to avoid repeating an expensive computation on every render, you can use useRef to store the result of that computation

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 that is 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 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 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. Here’s an example:

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 inbuilt 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 will 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 only use refs 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.

9 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