useState vs useRef: How to Choose the Right React Hook

Sai HariSai Hari
4 min read

State management is a critical part of most front-end applications. In React, you have a few mechanisms to track state, but two common ones are the useState and useRef hooks.

If you're not familiar with these hooks, it can be confusing to know when to use each one. In this post, we'll compare the useState and useRef hooks and build some basic intuition around when to choose each one.

The useState hook lets you add state to functional components.

The useState hook is the basic building block to manage state for functional components. The hook returns a tuple where the first element is the state and the second element is a function to update that state.

The key part of the updater function is that it's designed to trigger a re-render. When a React component re-renders, the JSX is recalculated and the underlying HTML is updated to reflect any state changes.

As you click the button in the example below, you'll notice that:

  1. The count variable is incremented

  2. The component re-renders

  3. The HTML changes

Great!

We can manage state in React now, but how does useRef come into play?

The useRef hook lets you track state that isn't needed for rendering.

Just like the useState hook, the useRef hook lets you track state. The hook returns an object with a current property that contains the state. To update the state, you just need to update the current property.

The key difference is that any update to this state won't trigger a re-render.

Here's a similar example as above, but with the useRef hook instead. There's also a button to force the component to re-render.

When you click the count button, notice how the state doesn't update. This is because updates to useRef values don't trigger re-renders.

That said, the underlying state is still updating.

But how do we prove that?

Well, if we use the other button to force a re-render, you'll notice that the button's value changes based on the current state tracked by the useRef hook:

Note: Because updates to useRef values don't trigger re-renders, you want to make sure you don't use a ref's value in your JSX. I know I'm breaking that rule above, but the example is just for demonstrative purposes.

Ok! So we want to use a ref when we track state that doesn't trigger a re-render. But what kind of state shouldn't trigger a re-render?

Let's take a look at a couple of examples.

How to scroll an element into view.

It's common to track DOM elements with the useRef hook. If you pass your ref variable to an element's ref prop, React will automatically set your variable's current property to the DOM element.

Once you have access to the DOM element, you can do various things like reset a form input or scroll an element into view.

In the example below, we have a dummy recipe blog post. Like most recipe blog posts these days, there's content before the recipe that provides some background about the dish.

Wouldn't it be nice if there were a button that let you skip all that extra information and jump straight to the good stuff?

Let's see how we might be able to do that with the useRef hook.

Note: I used ChatGPT to generate a dummy recipe blog post, so apologies if there are mistakes in the content.

Notice how we're passing the recipeRef to the h2 element Creamy Spinach and Mushroom Alfredo Pasta. React automatically sets the current value of our ref to the h2 DOM element. With that in place, we can use the ref to scroll the element into view when the button is clicked.

Easy!

How to build a countdown timer.

Another good use case for the useRef hook is to keep track of an interval ID that's returned from a setInterval call.

Let's say we had a countdown timer that shows the countdown in seconds. The easiest way to build this would be to use the useState hook to show the current timer value and then update the state every second.

That part is easy.

But what if we wanted a way to start and stop the timer? This is a little bit more tricky. The good news is that we can use the useRef hook for this. We can keep track of an interval ID when the timer starts and use it to clear the interval when the timer stops.

Let's see what that looks like:

Note: When we update the count, we're using a functional update because the function we pass to setInterval will have a stale version of the count variable due to its closure.

When to use each hook.

Hopefully, the examples above will help you build intuition around when to use each hook. But as a general rule of thumb use the useState hook when you want your component to re-render and the useRef hook when you don't want your component to re-render.

1
Subscribe to my newsletter

Read articles from Sai Hari directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sai Hari
Sai Hari

Content about improving developer process, tooling and code. Level 4 Vim Sommelier | I think, therefore I program