Tutorials

React useRef Hook for Dummies: How to Use useRef Correctly with Examples

Photo by Jason Leung on Unsplash

React is built around reactivity. You make a change or update a value, and the UI updates instantly. Almost every functional component in React has some state. Today we will discuss how we can use the React useRef hook correctly and explore different use cases for useRef.

Whenever you create a new function, you will most likely assign some state to it so that your UI would re-render whenever your data changes.

const [counter, setCounter] = useState(0);

Later if you want to update the counter:

setCounter(counter => counter + 1);

And then you are representing your state somewhere in your UI.

return (
    <div className="App">
      <p>{counter}</p>
    </div>
  );

But let's say that you wanted to store a value and update it without re-rendering your component. The simplest option would be to define a variable in top level scope of your component.

Define a variable in the component scope

Suppose that you wanted to store the previous value of counter in a prevCount variable whenever you updated counter. Normally, you'd go about it like this:

function Counter(props) {
let prevCount = 0; const [counter, setCounter] = useState(0);

// ... }

And when you are ready to increment your counter, you'll store the previous value like this:

return (
  <div>
      <p>{counter}</p>
      <button
        onClick={() => {
          setCounter((counter) => {
          prevCount = counter; // store previous value in prevCount
            return counter + 1; // increment
          });
        }}
      >
      Increment counter
      </button>
  </div>
);

Looks good, no? And this should work but see what happens to the prevCount across component renders.

Let's use a useEffect hook to print the value of prevCount to the console whenever we update the counter state:

useEffect(() => {     
  console.log("counter:", counter, "prevCount:", prevCount); 
}, [counter]);

And after building and a running:

Wait a second! Why is prevCount always 0?!

React rendering for dummies

A React functional component is essentially a Javascript function that returns a React component. This means that all code within it is run every time the component is rendered. This sounds inefficient but React is clever enough to persist its own state across component renders but for that to happen you, the developer, have to use things like useState.

In comparison, local variables inside a React functional component are not the responsibility of React since they are not a part of React component state. This means that whenever a component renders, like in our example when we increment the counter, all the local variables will be reset.

At this point you might be wondering, "Well, why don't we just use useState then?" The reason is that useState is the wrong tool for the job if the value you want to persist across renders doesn't affect it in any way. You'll be causing unnecessary re-renders every time you update the value.

So how can you make sure your local variables are persisted across component renders without triggering any React re-renders?

Define a variable outside component scope

AKA define your variable in the global scope. I know, I know, this is a bad practice for good reasons but let's consider it for the sake of completeness.

Javascript global scope is a double edged sword. Anything you define in the global scope is accessible anywhere else throughout the lifetime of the program. That means you can define a global constant in one place and update it from wherever you want. This also means that the garbage collector cannot cleanup the memory taken by the stuff in the global scope.

The only change you need to make is shift the prevCount variable to the global scope:

var prevCount = 0; // define prevCount in the global scope.

const Counter = () => {
....

Alright, that's simple but does it work?

Yep. It's working but there are a few problems with this approach:

  1. If the component unmounts, the value of prevCount will not reset to 0. You will manually need to reset the value every time — and there is no simple way to do that.
  2. If you reuse your Counter in multiple places, all the instances will reference the same prevCount value. If you haven't dropped this idea yet, this should be enough but for dummies, this means that whenever you update the counter state in one instance, it'll update the prevCount variable for ALL components.

On the surface, this approach solves our problem but if you look deeper we have ended up with more problems than before. Is there a way to do all this without all this fuss?

React useRef hook

From the React docs:

useRef returns a mutable ref object whose current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.

In English, this means you can assign any value to current property of useRef hook and update it without causing a re-render.

Reusing our Counter example, let's try to store the prevCount in a useRef hook:

const prevCount = useRef(0);

You'll also have to update your increment logic & your useEffect hook. The final code would look like this:

const Counter = () => {
  const prevCount = useRef(0);
  const [counter, setCounter] = useState(0);
  useEffect(() => {
    console.log("counter:", counter, "prevCount:", prevCount.current);
  }, [prevCount, counter]);
  
  return (
    <div className="App">
      <p>{counter}</p>
      <button
        onClick={() => {
          setCounter((counter) => {
            prevCount.current = counter; // store previous value in prevCount
            return counter + 1; // increment
          });
        }}
      >Increment counter</button>
    </div>
  );
};

Does it work?

Hell yeah it works! And you get multiple benefits:

  1. Our prevCount value is persisted across re-renders.
  2. We can update it easily by setting new value to .current.
  3. The value will automatically reset when our component unmounts.
  4. We can reuse our Counter component without any issue since each component will have it's out prevCount ref.

Common use cases of React useRef hook

Obviously, the above example is a little too simple. In real world use case, you'd probably not use useRef like that. So how would you use it?

Accessing DOM elements directly

React was specifically made to avoid this. Direct DOM access is a bad idea for many reasons. There are very few cases where you can't use React state to update a value or a style. Despite that, React useRef can be useful — especially if you want to use the DOM functions of an element like focus, blur etc.

Suppose you wanted to focus an input element when the counter reaches 10. You could easily do that with useRef like this:

const Counter = () => {
  const inputRef = useRef();
  const [counter, setCounter] = useState(0);

  useEffect(() => {
    if (counter === 10 && inputRef.current) {
      inputRef.current.focus();
    }
  }, [counter]);

  return (
    <div className="App">
      <p>{counter}</p>
      <button
        onClick={() => {
          setCounter((counter) => {
            return counter + 1; // increment
          });
        }}
      >
        Increment counter
      </button>
<br/>
<br/>
      <input ref={inputRef} />
    </div>
  );
};

Conclusion

React useRef hook is much more "useful" than you might think at first. One thing to note here is that it is just a Javascript object — it can store anything that you need to update, and keep track of, without causing a re-render.

However, this doesn't mean you ditch useState for useRef. Each has its uses. React is not a slow web component framework and it is carefully built to account for many performance issues. Although useState seems like a huge step back from "just update the value", it keeps things very predictable — single definition, single way to update it, single way to keep track of updates.

On the other hand, useRef is useful when you need to access functions of a component or "secretly" change the style of a component without a re-render.

In the next articles, I'll discuss the most common ways React useRef hook is misused. Till then, have a good day!

References