From the Development Team

May 7, 2020

Using React Refs to Manipulate the DOM

Paige Willey
By Paige Willey

What are refs?

At the most basic level, a ref is an object with a “current” property that does not reset with re-renders.

On the DOM level, a ref is an attribute on an element that allows you to refer (pun intended) to that particular element at a point in the typical lifecycle. For our purposes, refs are React's equivalent to the vanillaJS document.querySelector. A ref used with the DOM allows for imperative manipulation of an element rather than declarative. It's like driving your friend to their destination for them instead of giving them directions. If you told your friend to go straight and make a turn at the third left, they’d have to make sure they don’t miss any streets and know how to keep count to avoid getting off track. If you drive them instead, your familiarity with the landscape makes it so they’re just along for the ride. With a ref, you get to drive the ref element and tell it exactly what to do.

Why and when should I use them?

Why use refs instead of document.querySelector or its document.queryById? It’s because of React, JSX, and the lifecycle pipeline. When you hear people refer to the React ecosystem, these are parts of that ecosystem. Refs make working within that ecosystem a little more smooth.

In React, you typically interact with elements when there is a data change, which causes our typical lifecycle events like mounting and unmounting. document.querySelector happens outside of that lifecycle, making what it returns unreliable, while refs happen within it. (Though doesn’t get reset because of a lifecycle event like a re-render.) This ensures the object returned by the ref is an accurate representation of the current state of the virtual DOM.

When should you use them for the DOM?

If you've ever needed a lot of control over a UI element or wished you could reach your hands through the screen and manipulate it yourself, refs are your friend.

The official React documentation says this about when you should use refs:

*Managing focus, text selection, or media playback. Triggering imperative animations. Integrating with third-party DOM libraries.*

I've used refs to help me solve several UI problems on client projects while at UB.

(Note: The documentation states you can use the useRefs hook for things other than refs! We’re not going to go into that here, but if you’re curious, you can take a look in the documentation. )

In a more recent use case, a client needed to click from a list of items, open a modal containing expanded versions of all those items, and then scroll to the originally chosen item.

Getting the modal to open was easy. But scrolling after this had taken place? Less successful. So we used refs to make sure the modal had opened and then we grabbed the offset to scroll.

How should you use them?

You can follow along with this process below or check out the code sandbox. (https://codesandbox.io/s/objective-fast-vhc27?file=/src/modalAndButton.jsx:531-648)

For this example, we’ll render a list of quotes in a modal. We will be able to hide or show this modal by clicking on a button corresponding to the quote we want to see.

We’ll use the refs hook from React. When I first started out, refs were a little less user friendly than they are now with hooks. I personally find using the refs hooks much easier to use than non-hooks versions.

If you look up the refs hooks, you’ll find two different methods for refs. createRef() and useRef(). What’s the difference? Well, creatRef doesn’t work with functional components. Remember how I mentioned that a ref is an object with a ‘current’ property? When used in a class component, createRef gets the instance of the component as its ‘current’ property. The instance includes things like state and the lifecycle. Functional components don’t have instances. If that’s all too much, just remember that createRef for class components and useRef for functional components. Since I’m writing a functional component, useRef is for me.

I’ll also use Material UI components for this because 1) it’s easy and 2) it most closely resembles the design system of the client for whom I originally wrote this. It comes with an out-of-the-box modal component that is super easy to use. You just need to pass a boolean to it to know whether it should be open or not and a toggling function.

Once you’ve installed Material UI for React, you’ll get started with these three things:

A data set that’s preferably an array of objects where each object has an id (very important), and some content. A modal component A parent of the modal component that contains “links” to each of the data points.

Here’s how that looks in the parent component

import React, { useState, useRef } from "react";

import Modal from "./modal";

import { quotes } from "./quotesData";

export default () => {
  const [modalOpen, toggleModal] = useState(false);

  const openModal = () => {
    toggleModal(true);

  };
  return (
    <>
      {quotes &&
        quotes.map(item => (
          <div>
            <button onClick={() => openModal()}>{item.title}</button>
          </div>
        ))}

      <Modal
        open={modalOpen}
        toggleModal={toggleModal}
      />
    </>
  );
};

Let’s break this down from the top:

I have a hook for toggling the modal open or closed. The openModal function will come into play more later. I map over the quote data to render a button for each quote. Each button has the potential to open the modal. And below this we bring in the modal from the component we created for it.

That modal component looks like this right now:

import React from "react";
import Modal from "@material-ui/core/Modal";

import { quotes } from "./quoteData";

export default ({
  open,
  toggleModal,
}) => {
  const handleClose = fn => () => {
    fn(false);
  };
  return (
    <Modal
      width="medium"
      open={open}
      onClose={handleClose(toggleModal)}
    >
      <div>
        {quotes.map(item => (
          <div>
            <h1>{item.title}</h1>
            <div>{item.text}</div>
          </div>
        ))}
      </div>
    </Modal>
  );
};

At this point, we should now be able to open our modal by clicking on one of the rendered buttons for a particular quote. But the modal doesn’t scroll to the one we click. So if we click on the third button, when the modal opens, we see the first one. Not great. This is where refs can help us.

We’ll create two refs for this. The first one will refer to the modal. We’ll use it to make sure the modal is open. The second one will refer to the specific quote we clicked, allowing us to get information about where it is so we can scroll to it.

The reason I check for the modal being open first rather than just checking to see if the quote exists is a personal preference. I found checking just for the quote would give varying offset numbers for scrolling when the modal opened. If your data is particularly fast, you may not need this.

Import the useRefs hook in the parent component (you’ll pass this down to the modal), and create your refs. It’ll look like this

const modalRef = useRef(null);
const quoteRef = useRef(null);

You will pass both of these refs to the component In both instances, you’ll want to check in the parent component to see if the refs exist, so that’s why we put them there and not in the modal.

Once you’ve passed both down, you’ll set them up by setting them equal to the ref attribute on the element you want to refer to. So you’ll have a ref for the modal, and a ref for the div that holds the quote.

  <Modal
    open={open}
    onClose={handleClose(toggleModal)}
    ref={modalRef}
    >
<div ref={quoteRef} />

Remember when I said having an id on the quote data was important? If you map over the data as our example does, this is where that becomes important. If you put the quoteRef on a single div in a map, without a unique key of some sort, you could accidentally refer to every element you mapped over. That’s not what we want.

So, we need to set up one more state hook to make sure we get just one ref.

    const [chosenQuote, setChosenQuote] = useState(null);

When you select the button for a particular quote, instead of opening the modal with toggleModal(), you will now use the openModal custom function we created earlier.

This custom function will set the id of the quote to chosenQuote.

We can also open the modal and check for both refs in this function.

Something like this

const openModal = id => {
    setChosenQuote(id);
    toggleModal(true);
  };

Okay! Almost done. Now for the important logic: check for the refs and add the scroll.

You’ll pass chosenQuote to the modal component and set it up on the quote’s element like this

{quotes.map(item => (
          <div ref={item.id === chosenQuote ? quoteRef : null}>
            <h1>{item.title}</h1>
            <div>{item.text}</div>
          </div>
        ))}

See what’s happening here? We use the id and our chosenQuote variable to only set the ref on the item we absolutely want. There are other ways to do this, but this is my preference.

From here, we can now create our final function and the most important piece: The function to check and scroll the modal! (kudos to you for sticking around this long).

We’ll put it in the parent component, and it looks like this

const checkForModal = () => {
    if (modalRef && quoteRef && quoteRef.current) {
        quoteRef.current.scrollIntoView({
          behavior: "smooth"
        });
    }
  };

So this says if the modal’s ref exists (if the modal has been opened and is visible on the page), AND if the quote’s ref exists and has attributes, then smoothly scroll the quote into view. That ‘scrollIntoView’ is a specific number that tells the document (in this case the modal), to scroll a certain number of pixels. Note: I do not recommend using offsetTop here. The numbers were always wrong for me, for reasons I suspect but am too lazy to verify.

We’ll pop this into our openModal function on a small timeout so we know for sure that the elements are there. I prefer to put a tiny delay on my timeout because sometimes it can take elements a little while to load and get their heights. I find that when I leave off the setTimeout, sometimes one element above the desired element won’t be done loading by the time the scroll triggers. And then you end up scrolling to the wrong spot.

    const openModal = id => {
    setChosenQuote(id);
    toggleModal(true);

    setTimeout(checkForModal, 10);
  };

Once this function is on the buttons and the id of the quote is getting passed to it, you’re all done! Give it a go!

We just created a modal that renders data and learned how to scroll to a specific piece of data upon opening the modal. Through the use of refs, we checked to make sure the data was in the DOM and acquired a single piece's specific location to smoothy scroll to it automatically.

And that's it! We've successfully used refs to manipulate the DOM in a use case that’s just made for it!

You can view the complete code here: https://codesandbox.io/s/objective-fast-vhc27?file=/src/modalAndButton.jsx:531-648

And if your data is really fast and you don’t mind a couple more advanced concepts like the ref querySelector and forwardRefs, you can check it out here: https://codesandbox.io/s/cool-villani-t4f82