Kyle Shevlin

Software Engineer
September 22, 2021
0 strokes bestowed

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.


Finished reading?

Here are a few options for what to do next.

Like
Liked the post? Click the beard up to 50 times to show it
Share
Sharing this post on Twitter & elsewhere is a great way to help me out
Support
Was this post valuable to you? Make a donation to show it
Make a Donation
Kofi logo

Newer Post: Careful with Context Composition
Older Post: What is a Closure?
Tags
ReactJavaScript

Kyle Shevlin's face, which is mostly a beard with eyes
Kyle Shevlin is a software engineer who specializes in JavaScript, React and front end web development.

Let's talk some more about JavaScript, React, and software engineering.

I write a newsletter to share my thoughts and the projects I'm working on. I would love for you to join the conversation. You can unsubscribe at any time.

Array.reduce() Logo
Array.reduce()

Check out my courses!

Liked the post? You might like my courses, too. Click the button to view this course or go to Courses for more information.
I would like give thanks to those who have contributed fixes and updates to this blog. If you see something that needs some love, you can join them. This blog is open sourced at https://github.com/kyleshevlin/blog
alexloudenjacobwsmithbryndymentJacobMGEvanseclectic-codingjhsukgcreativeerikvorhesHaroenvmarktnoonandependabotmarcuslyonsbrentmclarkfederico-fiorinimedayzDoNormal
©2021 Kyle Shevlin. All Rights Reserved.