DEV Community

Cover image for Introduction to useRef Hook
TrinhDinhHuy
TrinhDinhHuy

Posted on • Edited on

React useRef Introduction to useRef Hook

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>
   )
}
Enter fullscreen mode Exit fullscreen mode

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>
      </>
   )
}
Enter fullscreen mode Exit fullscreen mode

πŸ€” Wait! What's the difference?

dopperlganger

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>
      </>
   )
}
Enter fullscreen mode Exit fullscreen mode

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

compare useRef and createRef

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

πŸ’ͺ 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:

πŸ™ πŸ’ͺ 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

Oldest comments (8)

Collapse
 
hd4ng profile image
Huy Dang

Nice article. Very easy for me to understand useRef. Thanks.

Collapse
 
cuiyajie profile image
cuiyajie

I learned other way to use ref hook except ref for component or dom element. Thank you a lot!

Collapse
 
kethmars profile image
kethmars

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).

Collapse
 
dinhhuyams profile image
TrinhDinhHuy

Thank you for sharing

Collapse
 
andyneale profile image
Andy Neale

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?

React.useEffect(() => {
  ref.current = value
}, [value])
Collapse
 
dinhhuyams profile image
TrinhDinhHuy • Edited

Yes, it should. Thankss, I updated the article

Collapse
 
vladpischaeff profile image
Vlad-Pischaeff

Very useful article.
But in this case goal achieved by more simply way.
Just add [user.weight] to dependencies of useEffect.

Collapse
 
yael95 profile image
Yahya Elfaqir

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?