Skip to content

Instantly share code, notes, and snippets.

@HuyAms
Created September 10, 2019 11:41
Show Gist options
  • Save HuyAms/782553a6e275e4bfc694a6a3a6d1a46f to your computer and use it in GitHub Desktop.
Save HuyAms/782553a6e275e4bfc694a6a3a6d1a46f to your computer and use it in GitHub Desktop.
## Prerequisite: Basic knowledge about [React](https://reactjs.org/) and [Refs and the dom](https://reactjs.org/docs/refs-and-the-dom.html#callback-refs) 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**
```jsx
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**
```jsx
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?
![dopperlganger](https://user-images.githubusercontent.com/26871154/64486033-bb0d8780-d230-11e9-8311-042dc7028496.jpg)
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.
```jsx
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
![compare useRef and createRef](https://media.giphy.com/media/LndDkp00MVrF9ECX55/giphy.gif "compare useRef and createRef")
> You can find this comparation of **useRef** and **createRef** in Ryan Cogswell's answer on [stackoverflow](https://stackoverflow.com/questions/54620698/whats-the-difference-between-useref-and-createref)
πŸ‘ 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 πŸ˜‡
```jsx
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
```jsx
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
```jsx
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**
```jsx
const usePrevious = (value) => {
const previousUserRef = React.useRef()
React.useEffect(() => {
previousUserRef.current = value
})
return previousUserRef.current
}
```
And to make it more generic, I will rename `previousUserRef` to `ref`
```jsx
const usePrevious = (value) => {
const ref = React.useRef()
React.useEffect(() => {
ref.current = value
})
return ref.current
}
```
Let's apply our custom **usePrevious** hook to the code
```jsx
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](https://github.com/kentcdodds/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](https://medium.com/@accardo.steven/throw-the-flag-reacts-createref-api-callbackref-api-and-forwardref-api-cb464231cca1)
- [React useRef documentation](https://reactjs.org/docs/hooks-reference.html#useref)
- [Handle Deep Object Comparison in React's useEffect hook](https://egghead.io/lessons/react-handle-deep-object-comparison-in-react-s-useeffect-hook-with-the-useref-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](https://github.com/HuyAms)
βœ… [LinkedIn](https://www.linkedin.com/in/huy-trinh-dinh-253534131/)
βœ… [Medium](https://medium.com/@trnhnhhuy)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment