Introduction to useRef Hook

Huy Trinh
Shot code
Published in
6 min readSep 11, 2019
Photo by David Marcu on Unsplash

Prerequisite: Basic knowledge about React and Refs and the dom in React

This post is going to talk about what is useRef hook and when we can use it. If you wonder why you should use Hooks, Why React Hooks Part I and Part II are just the right places for you

The first time I learned Hooks, I have so many questions that I need to look for the answers. One of those questions is how I can compare the current state/props with the previous one or handle deep object comparison in useEffect hook. I would only figure it out when I learned about useRef hook then every pieces fall into place.

💪 Let’s get started!

1. What is useRef Hook?

Refs provide a way to access DOM nodes or React elements created in the render method.

Our example is about managing the focus of an input when the user clicks on the button. To do that, we will use the createRef API

createRef API

import {createRef} from 'react' 

const FocusInput = () => {
const inputEl = createRef()

const focusInput = () => {
inputEl.current.focus()
}

return (
<>
<input ref={inputEl} type="text" />
<button onClick={focusInput}>Focus input</button>
</div>
)
}

We can achieve exactly the same result with useRef hook

useRef Hook

const FocusInput = () => {
const inputEl = React.useRef()

const focusInput = () => {
inputEl.current.focus()
}

return (
<>
<input ref={inputEl} type="text" />
<button onClick={focusInput}>Focus input</button>
</>
)
}

🤔 Wait! What’s the difference?

I asked the same question when I first read about useRef. Why do we need to use useRef hook when we can use createRef API to manage the focus of an input? Does the React team just want to make the code look consistent by creating a doppelganger when they introduced Hooks in React 16.8?

Well, the difference is that createRef will return a new ref on every render while useRef will return the same ref each time.

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.

const Test = () => {
const [renderIndex, setRenderIndex] = React.useState(1)
const refFromUseRef = React.useRef()
const refFromCreateRef = createRef()

if (!refFromUseRef.current) {
refFromUseRef.current = renderIndex
}

if (!refFromCreateRef.current) {
refFromCreateRef.current = renderIndex
}

return (
<>
<p>Current render index: {renderIndex}</p>
<p>
<b>refFromUseRef</b> value: {refFromUseRef.current}
</p>
<p>
<b>refFromCreateRef</b> value:{refFromCreateRef.current}
</p>

<button onClick={() => setRenderIndex(prev => prev + 1)}>
Cause re-render
</button>
</>
)
}

As you can see, refFromUseRef persists its value even when the component rerenders while refFromCreateRef does not

> You can find this comparation of useRef and createRef in Ryan Cogswell’s answer on stackoverflow

👏 Interesting! useRef can hold a value in its .current property and it can persist after the component rerenders. Therefore, useRef is useful more than just managing the component ref

2. Beyond the Ref attribute

Apart from the ref attribute, we can use useRef hook to make a custom comparison instead of using the default shallow comparison in useEffect hook. Take a look at our example 😇

const Profile = () => {
const [user, setUser] = React.useState({name: 'Alex', weight: 40})

React.useEffect(() => {
console.log('You need to do exercise!')
}, [user])

const gainWeight = () => {
const newWeight = Math.random() >= 0.5 ? user.weight : user.weight + 1
setUser(user => ({...user, weight: newWeight}))
}

return (
<>
<p>Current weight: {user.weight}</p>
<button onClick={gainWeight}>Eat burger</button>
</>
)
}

export default Profile

Provided that the user’s name will always unchanged. Our expectation is that the effect will output the warning text only when the user has gained weight. However, if you test the code above, you can see that our effect runs every time the user clicks on the button, even when the weight property stays the same. That is because useEffect Hook uses shallow comparison by default while our userState is an object. 🐛🐛🐛

🔧 To fix this bug, we need to write our own comparison instead of using the default one.

👉 Step 1: use lodash isEqual method for deep comparison

const Profile = () => {
const [user, setUser] = React.useState({name: 'Alex', weight: 40})

React.useEffect(() => {
if (!_.isEqual(previousUser, user) {
console.log('You need to do exercise!')
}
})

...
}

export default Profile

We have just removed the dependency array in our effect and use the lodash isEqual method instead to make a deep comparison. Unfortunately, we run into a new issue because of the missing previousUser value. If we do the same thing with a class component in ComponentDidUpdate lifecycle, we can easily have the previous state value.

🔥 useRef comes to rescue

👉 Step 2: useRef for saving the previous state

const Profile = () => {
const [user, setUser] = React.useState({name: 'Alex', weight: 20})

React.useEffect(() => {
const previousUser = previousUserRef.current
if (!_.isEqual(previousUser, user) {
console.log('You need to do exercise!')
}
})

const previousUserRef = React.useRef()
React.useEffect(() => {
previousUserRef.current = user
})

...
}

export default Profile

To keep track of the previousUser value, we save it to the .current property of useRef hook because it can survive even when the component rerenders. To do that another effect will be used to update the previousUserRef.current value after every renders. Finally, we can extract the previousUser value from previousUserRef.current, then we deep compare the previous value with the new one to make sure our effect only run when those values are different

👉 Step 3: extract effects to the custom Hooks

If you want to reuse the code, we can make a new custom hook. I just extract the code above to a function called usePrevious

const usePrevious = (value) => {
const previousUserRef = React.useRef()
React.useEffect(() => {
previousUserRef.current = value
}, [value])

return previousUserRef.current
}

And to make it more generic, I will rename previousUserRef to ref

const usePrevious = (value) => {
const ref = React.useRef()

React.useEffect(() => {
ref.current = value
}, [value])

return ref.current
}

Let’s apply our custom usePrevious hook to the code

const Profile = () => {
const initialValue = {name: 'Alex', weight: 20}
const [user, setUser] = React.useState(initialValue)

const previousUser = usePrevious(user)

React.useEffect(() => {
if (!_.isEqual(previousUser, user) {
console.log('You need to do exercise!')
}
})

const gainWeight = () => {
const newWeight = Math.random() >= 0.5 ? user.weight : user.weight + 1
setUser(user => ({...user, weight: newWeight}))
}

return (
<>
<p>Current weight: {user.weight}</p>
<button onClick={gainWeight}>Eat burger</button>
</>
)
}

export default Profile

💪 How cool is that! You can also extract the deep comparison logic to a new custom Hook too. Check out use-deep-compare-effect by Kent C. Dodds

3. Conclusion:

🚀 useRef Hook is more than just to manage DOM ref and it is definitely not createRef doppelganger. useRef can persist a value for a full lifetime of the component. However, note that the component will not rerender when the current value of useRef changes, if you want that effect, use useState hook instead 👏👏👏

Here are some good resources for you:
Reacts createRef API
React useRef documentation
Handle Deep Object Comparison in React’s useEffect hook

🙏 💪 Thanks for reading!

I would love to hear your ideas and feedback. Feel free to comment below!

✍️ Written by

Huy Trinh 🔥 🎩 ♥️ ♠️ ♦️ ♣️ 🤓

Software developer | Magic lover

Say Hello 👋 on

Github

LinkedIn

Medium

--

--