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.
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 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 user has gained weight. However, if you test the code above, you can see that our effect run every time the user clicks on the button, even when the weight
property stays the same. That is because useEffect Hook use 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 comparision
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 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
Top comments (8)
Great article, a lot of things suddenly make sense to me now!
Just one minor observation - should the effect have the value as a dependency so that it doesn't run on every re-render?
Yes, it should. Thankss, I updated the article
Hey hey!
Thank you for this awesome article. This combined with React docs made it clear what refs are. The article was actually so good I references you in my own article:
dev.to/kethmars/today-i-learned-re...
I hope it's okay to borrow your animation(again, referenced).
Thank you for sharing
What if for instance I have multiple buttons and I want to add a component when a particular button is clicked, eg sorting by title, author ..., each of these have a sort button and I want to show an up or down arrow next to each ACTIVE one not all of them, how can I use useRef to achieve that?
Very useful article.
But in this case goal achieved by more simply way.
Just add [user.weight] to dependencies of useEffect.
Nice article. Very easy for me to understand useRef. Thanks.
I learned other way to use ref hook except ref for component or dom element. Thank you a lot!