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!