React Refs: All You Need To Know About It

Elijah
June 7, 2022

TABLE OF CONTENTS

React ref provides a means of accessing DOM elements in this article you will learn how and when to use refs, and how refs can be useful outside the DOM.

In vanilla JavaScript, we often find ourselves accessing(selecting) DOM elements to perform some action like playing(controlling) an audio/video, changing the scroll top of an element, or maybe just reading an element's value. And all of this we do with the different methods (querySelector, getElementById, etc) of the document object.

While this is possible in React, it is not the best approach. But there is a better (React-like) approach for accessing DOM elements, and that is with React refs.

What is React Ref?

React ref provides a means of accessing DOM elements without using any selector methods of the document object. This is possible because React provides a ref JSX attribute on every JSX element, and with it, you can pass a ref variable to it and then have access to that element. We will see how this is done soon.

React ref doesn't just work on DOM elements only, it also works on class-based components (but not function components) using the ref JSX attribute. This is because class-based components have instances but function components don't. If you'd want to make a ref to your components regardless of the type, you can use forwardRefs to do that. But then, the need to create a reference of your components may never come. We will see some use-cases of refs and we will talk about why you may not need them for your components subsequently.

Another interesting thing about React refs is that it can also work outside of the ref JSX attribute. In fact, with refs, you can create, access, and update a value without rerendering the component, unlike states. Say you want to update a value without having to rerender your component, you can't use state because states will rerender the component. This is the difference between refs and states.

How to Create Refs

Creating refs is straightforward. We can create refs both in function components and class-based components. And there are three different methods for this

  1. The useRef hook
  2. createRef
  3. Callback refs

Creating callback refs is a lot more encompassing than the others, so to not overwhelm you already, we would talk about that later.

The useRef Hook

This only works for function components because it's a React hook, like in the example below:


import { useRef } from 'react';

function App() {
  const titleRef = useRef();

  return (
    <div>
      <h2 ref={titleRef}>Hello World!</h2>
    </div>
  );
}

Firstly, we import the useRef hook from React, call the function and assign it to a variable titleRef. Lastly, we pass the titleRef variable in the ref JSX attribute of h2.

As mentioned before the ref attribute is pretty much available to every JSX element, including class-based components. We could as well pass the titleRef to the ref attribute of the div element above.

The useRef hook can accept an argument as an initial value for the ref being created. This can be useful for creating ref values that are not for accessing DOM elements.

createRef

This is mostly used in class-based components but works also for function components. It works just like the useRef hook.


import { createRef } from 'react';

function App() {
  const titleRef = createRef();

  return (
    <div>
      <h2 ref={titleRef}>Hello World!</h2>
    </div>
  );
}

This is pretty much the same as the useRef only the createRef function changed. Unlike the useRef hook, the createRef function does not accept any arguments.

Choosing between useRef and createRef should really be based on preference, I think if you're working with function components it would be more generic or custom to use the useRef hook being that hooks were made for function components. And then if you're using class-based components, as of now you only have the createRef option because hooks don't work on class-based components.

Now that we've seen how to create refs, let's now see how to access the refs we have created.

How to Access Refs

We can access React refs as such:


console.log(titleRef.current);

We can access the value of any created ref with the current property. This gives us the current value of the ref. It doesn't matter what you use to create the ref (useRef, createRef), we can access the value with the current property.

In our case, we would have:


import { useRef, useEffect } from 'react';

function App() {
  const titleRef = useRef();

  useEffect(() => {
    console.log(titleRef.current); // logs: <h2>Hello World!</h2>
  }, []);

  return (
    <div>
      <h2 ref={titleRef}>Hello World!</h2>
    </div>
  );
}

The value of the current value of the ref will be the value of the DOM element it (the ref) was used on. And this happens when the components mount, this is why we have used the useEffect hook. When the component unmounts React will assign the current property to null.

So whether you're using class-based components or functional components you want to make sure you access the value of the ref when the component has mounted.

When to use ref

Now we have seen how to create and access React refs, it would be helpful to see some use-cases of React refs. Not every time you will need React refs, as much as it is powerful it can also be limiting when you use it for the wrong purpose. So "When should I use React refs?"

You should only use React refs for things that cannot be done declaratively. Not just for things that can be done imperatively. Because while some things can be done declaratively and imperatively you should always pick the former in React.

Let me explain, with declarative programming you write codes that describe what you want, without any step-by-step guide (instructions) on achieving your desired result. For example in React, you add an onClick listener on an element to perform an action:


// ...
return (
  <div>
    <button onClick={(e) => console.log(e.target.value)}>Click me!</button>
  </div>
);
// ...

On the other hand, with imperative programming, you write codes that describe what you want with a step-by-step guide. For example, rather than using the onClick attribute, we use a selector or in this case refs:


// ...
const btnRef = useRef();
useEffect(() => {
  btnRef.current.addEventListener('click', (e) => console.log(e.target.value)); // don't use ref for this, this is just an example
}, []);

return (
  <div>
    <button ref={btnRef}>Click me!</button>
  </div>
);
// ...

The same logic in both examples, but different approaches. The second example manipulates the DOM directly whereas the first doesn't even touch the element, it simply declares an element should be rendered given our current state. Now we can see that this example can be done imperatively or declaratively, but you should always choose to do such things declaratively in React.

Developers always tend to use refs for things that can be done declaratively like the last example above, you shouldn't. Most things in React can be done declaratively, refs were made for things that cannot be done declaratively, like focusing on an input field when a user clicks a button (we will see more use-cases in a moment).

If you're still not satisfied with the explanation of declarative and imperative programming, here is a perfect metaphor on StackOverflow that can help out.

Common Use-cases of React refs

  1. Managing input focus, text selection
  2. Imperative animation
  3. Media control/playback
  4. Outside the DOM
  5. With third-party libraries

Let's look at some examples of these use-cases

1. Managing Input Focus

This is a very common use case in ref:


function App() {
  const inputRef = useRef();

  const focusOnInput = () => {
    inputRef.current.focus();
  };

  return (
    <div>
      <input type='text' ref={inputRef} />
      <button onClick={focusOnInput}>Click Me</button>
    </div>
  );
}

In the example above, when a user clicks the button the input text will be focused. Now you may want to be tempted to do autofocus when the component mounts like:


//...
useEffect(() => {
  inputRef.current.focus();
}, []);
//...

But then, this can be done declaratively with the autoFocus attribute for input elements in JSX, so you should avoid doing it imperatively.

2. Imperative animations

You can use third-party libraries for imperative animations e.g the shake method of react-native-elements. With this library, you can shake a user's device when input is invalid on React Native. Since this cannot be done declaratively, then you can use refs for it.

3. Media control/playback

There is no declarative way of playing audio when a user clicks a button. In vanilla JavaScript, the solution would be to select the audio and then play/pause it, but in React we can simply use refs:


function App() {
  const audioRef = useRef();

  const playAudio = () => {
    audioRef.current.play();
  };

  const pauseAudio = () => {
    audioRef.current.pause();
  };

  return (
    <div>
      <audio
        ref={audioRef}
        type='audio/mp3'
        src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/1506195/keyboard-32-12.mp3'
      ></audio>
      <button onClick={playAudio}>Play Audio</button>
      <button onClick={pauseAudio}>Pause Audio</button>
    </div>
  );
}

The same thing applies when you use libraries like video.js.

4. Outside the DOM

Recall, we mentioned earlier that we could use refs without the ref attribute or better say without creating a reference to an element. This is where we made mention of using the useRef's only argument which is for setting initial values.

So we can create values for our refs without using them in the DOM, for example, we can get the number of times a component renders with refs.


function App() {
  const numOfRenders = useRef(0);
  const [name, setName] = useState('');

  useEffect(() => {
    numOfRenders.current += 1;
    console.log(numOfRenders.current);
  });

  return (
    <div>
      <input
        type='text'
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
    </div>
  );
}

As you know, the component will render initially when the component mounts, and will also render when a state is updated. In our case, the name state will cause our component to rerender as the user types in the input field. And as the component rerenders our ref value (initially 0) will update.

The reason we have decided to use refs instead of states is that when the ref updates it will not cause the component to rerender. If we had used state we would end up with an infinite rerendering.

Another similar example would be accessing the previous value of a state; these examples may not be always useful but it gives you an idea of when you can/should use refs:


function App() {
  const numRef = useRef(0);
  const [num, setNum] = useState(0);

  useEffect(() => {
    numRef.current = num;
  }, [num]);

  const incrementNum = () => {
    setNum((num) => num + 1);
  };

  return (
    <div>
      <p>Current Value: {num}</p>
      <p>Previous Value: {numRef.current}</p>
      <button onClick={incrementNum}>Increment num</button>
    </div>
  );
}

One other common example is in cleaning an async useEffect function during unsubscribing:


function App() {
  const isSubscribed = useRef(true);
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    (async () => {
      const res = await fetch(
        'https://jsonplaceholder.typicode.com/posts/_limit=10'
      );
      const jsonRes = await res.json();
      if (isSubscribed.current) {
        setPosts(jsonRes);
      }
    })();

    return () => {
      isSubscribed.current = false;
    };
  }, []);

  return (
    <div>
      <ul>
        {posts.map((post) => (
          <li>{post.title}</li>
        ))}
      </ul>
    </div>
  );
}

This way we are preventing React from setting the posts state when the component has unmounted.

When to not use ref

As much as possible, you should try to avoid manipulating the DOM directly which is basically what React ref does (except imperatively). Ian Mundy has this to say about ref:

“...if you feel yourself reaching for refs then it can often mean you’re doing something wrong.”

Nevertheless, all the examples we've seen are legitimate use-cases of refs, and apart from that, we've also seen that refs is all about doing something that cannot be done declaratively.

Some developers often use refs for passing values around components in place of props. This is a bad practice. If there is a need to pass a ref value to another component, you can use props instead of using the ref attribute on the component.

So let's see a list of the "refs are not for" we've seen so far

  1. Not for anything that can be declaratively
  2. Not for passing data around components
  3. Not for manipulating the DOM directly

Manipulating the DOM doesn't only mean adding an event listener as we've seen before it could also be about changing the value of the element:


function App() {
  const textRef = useRef(0);

  const changeValue = () => {
    textRef.current.innerText = 'Text has changed';
  };

  return (
    <div>
      <p ref={textRef}>Hello World</p>
      <button onClick={changeValue}>Change value</button>
    </div>
  );
}

Rather than doing that, you should use React states for it.

Callback refs

Callback ref is one of the ways of creating React refs, it is a lot different from the createRef and useRef functions.


function App() {
  const setTitleRef = (element) => {
    console.log(element);
  };

  return (
    <div>
      <h2 ref={setTitleRef}>Hello World!</h2>
    </div>
  );
}

The difference here is that instead of passing the ref variable from using createRef or useRef in the ref JSX attribute, we pass a callback function instead. This function takes an argument that represents the element.

Let's try one of the use-cases we've seen above with callback refs:


function App() {
  let inputElement = null;

  const focusOnInput = () => {
    inputElement.focus();
  };

  return (
    <div>
      <input type='text' ref={(e) => (inputElement = e)} />
      <button onClick={focusOnInput}>Click Me</button>
    </div>
  );
}

React will call the callback ref when the component mounts and assign the value to null when the component unmounts, just like we've seen with both createRef and useRef.

Conclusion

React ref is a nice feature when you use it right, so it's better to limit the way you use it. When using it try to see if it fits with any of the use-cases we've mentioned above. For example, you could want a variable that would be updated over time but during this update, you don't want your component to rerender, then you use refs. Also, it is useful for doing things that can't be done declaratively.

So far we've learned what refs are and why they exist in React, we've also looked at how to create and access refs with the three available methods. And most of all we've seen legitimate use-cases of refs, and also known when to use refs and when not to. Putting all these together you are good to go with React refs.