Comparing `useRef` and `useState`
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:
-
useState
-
useReducer
-
useContext
-
useRef
-
useEffect
-
useLayoutEffect
-
useCallback
-
useMemo
-
useRef
-
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.
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.