September 22, 2021

Comparing `useRef` and `useState`

edit

When I discuss React with people, I often hear that others find useRef to be confusing. People are uncertain about when and why they should use it. They often opt to use useState instead when perhaps it’s not the right choice. I think one way of gaining an understanding of useRef is to compare it with useState, so let’s do that.

Before we get into the specific differences, I want to share a realization I recently had. I realized that the standard React Hooks come in four categories:

State Managers
  • useState
  • useReducer
  • useContext
  • useRef
Effects
  • useEffect
  • useLayoutEffect
Stabilizers
  • useCallback
  • useMemo
  • useRef
Miscellaneous
  • useImperativeHandle
  • useDebugValue

Notice that I put useRef in two categories. It doesn’t fall neatly into one or the other. It holds a bit of state, whatever value is assigned to ref.current, making it a “State Manager”. But it’s also a “Stabilizer” because changing the value of ref.current does not cause a component to update.

useState, on the other hand, is designed to update a component. In fact, I like to help people understand useState better by showing them how to make a useForceUpdate hook with it.

If you have written React for long enough, or have had the chance to work or search through some class component code, you may have used or come across an invocation of this.forceUpdate(). React class components have a method, forceUpdate, that you can use to imperatively force a component to rerender. It was an escape hatch of last resort. The move to React Hooks meant we lost this escape hatch, right?

Wrong.

We can create that same imperative escape hatch function like so:

const useForceUpdate = () => {
  const [, setState] = React.useState(true)
  const forceUpdate = React.useCallback(() => {
    setState(x => !x)
  }, [])

  return forceUpdate
}

Let’s break this code down. First, we only destructure the state setter from the useState hook. We don’t actually need the state, we just need it to change whenever we want to force an update. Next, we make a callback function, stabilize it with useCallback, and use a function updater to flip the boolean back and forth. We then return our function from the hook. If we include this in a component, which we will see soon enough, we can force it to update. Let’s put this into practice.

As I’ve demonstrated before in my post on React.memo, one way to visually see that a component has updated is to give it a random background color with each render. Let’s make a component that does just that. First, the randomRGB function:

const random255 = () => Math.floor(Math.random() * 255)

const randomRGB = () => {
  const r = random255()
  const g = random255()
  const b = random255()

  return `rgb(${r}, ${g}, ${b})`
}

Next, we’ll make a box with a “Force Update” button that uses randomRGB.

function Box() {
  const forceUpdate = useForceUpdate()

  return (
    <div style={{ backgroundColor: randomRGB(), padding: 10 }}>
      <button onClick={forceUpdate}>Force Update</button>
    </div>
  )
}

And let’s see it in action.

Neat. Now, hopefully it’s clear what causes a rerender with useState. The question we must now ask ourselves is, “How does this relate to useRef?“. Let’s try to make a similar box, but use useRef instead. We’ll have to create our own state updater to do this since useRef doesn’t give us one.

function RefBox() {
  const ref = React.useRef(true)
  const flip = React.useCallback(() => {
    ref.current = !ref.current
  }, [])

  return (
    <div style={{ backgroundColor: randomRGB(), padding: 10 }}>
      <button onClick={flip}>Try to update</button>
    </div>
  )
}

I bet you just clicked that button repeatedly and wondered if it’s actually doing anything. I assure you it is, check your console. I snuck a console.log into flip on you.

useRef is a mutable value store. In other words, we can imperatively update the ref by assigning a new value to ref.current. However, a component does not update when ref.current changes. Some other kind of state update needs to happen in order for the component to rerender.

Let’s make an example that combines these two features together. We’ll make a box with a counter as a ref. We’ll update that counter in the background and see the current value of that ref whenever we force the component to update.

function BackgroundCounter() {
  const counter = React.useRef(0)
  const forceUpdate = useForceUpdate()

  React.useEffect(() => {
    const interval = setInterval(() => {
      counter.current++
    }, 100)

    return () => {
      clearInterval(interval)
    }
  }, [])

  const reset = () => {
    counter.current = 0
    forceUpdate()
  }

  return (
    <div style={{ backgroundColor: randomRGB() }}>
      <div>{counter.current}</div>
      <div>
        <button onClick={forceUpdate} type="button">
          Force Update
        </button>
        <button onClick={reset} type="button">
          Reset
        </button>
      </div>
    </div>
  )
}

And let’s see how that works.

0

Notice that the value displayed only updates when we force it to. It’s been changing in the background this entire time, and yet, because it was a ref, the component never updates. It was probably a pretty big number the first time you updated it, too. It’s been running since you started reading this article.

So when might we practically want to use useRef? Well, the answer is the combination of those facts, “Whenever we need a value that will always be available, can be changed when necessary, without updating the component.” I know that seems unhelpful, but let’s make another example.

useRef essentially allows us to create a value in closure for the lifetime of the component. If you need a refresher on closures, I got you. Have you ever had an effect that you wanted to ensure only ran once? We can make a useEffectOnlyOnce hook by using a ref.

function useEffectOnlyOnce(effect, dependencies) {
  const hasRun = React.useRef(false)

  React.useEffect(() => {
    if (hasRun.current) return

    hasRun.current = true
    effect()
  }, dependencies)
}

Or what about an effect that can only run after a component has initially mounted?

function useEffectAfterInitialMount(effect, dependencies) {
  const isMounted = React.useRef(false)

  React.useEffect(() => {
    if (!isMounted.current) {
      isMounted.current = true
      return
    }

    effect()
  }, dependencies)
}

Or, I don’t know, what about an effect that only runs every nth time because you just want to be silly?

function useEffectEveryNthTime(effect, dependencies, n) {
  const count = React.useRef(0)

  React.useEffect(() => {
    count.current++

    if (count.current % n === 0) {
      effect()
    }
  }, dependencies)
}

The point is a ref is very useful when you need to track a value for the lifetime of a component but don’t need it’s every change to update the UI. If you need that, you probably need to use a different state manager hook.

Summary

Changing useState’s value will always cause a rerender, even if that state isn’t used anywhere in the component. Changing useRef’s value will never cause a rerender, even if you use that state somewhere in the component.

useRef is useful for having a stable reference to a value throughout the lifetime of a component. It is a mutable value held in the closure of your component function and can be used by the functions created in the body of the component function.


Liked the post?
Give the author a dopamine boost with a few "beard strokes". Click the beard up to 50 times to show your appreciation.
Want to read more?
Related Post:
Kyle Shevlin's face, which is mostly a beard with eyes

Kyle Shevlin is the founder & lead software engineer of Agathist, a software development firm with a mission to build good software with good people.

Logo for Just Enough Functional Programming
Just Enough Functional Programming
Check out my courses!
If you enjoy my posts, you might enjoy my courses, too. Click the button to view the course or go to Courses for more information.
Sign up for my newsletter
Let's chat some more about TypeScript, React, and frontend web development. Unsubscribe at any time.