React Hooks Series: useRef

jamesncox

James Cox

Posted on August 20, 2020

React Hooks Series: useRef

Introduction

Make sure to check out my Timer CodeSandbox first. Play around with the timer, fork the sandbox, examine the code, and even refactor to make it better!

The previous two articles in my React Hooks Series broke down useState and useEffect. This post will focus on useRef, one of my favorite hooks. I readily admit that I am not a useRef expert by any means, and this article only covers how I implement the useRef hook in relation to my Timer example.

A Quick Detour

Let's discuss WHY I need the useRef hook in my Timer app.

It has to do with the PAUSE button and how it behaves. Initially I did not have useRef tied to my pause functionality. When the user tried to pause, there was often a delay and the timer would still tick down an additional second.

We should look at that specific behavior, because we can gain better understanding of useEffect and setTimeout also.

As a reminder, I conditionally render the PAUSE button when both start === true AND counter does not equal exactly 0.

{
   start === true && counter !== 0
   ? 
   <button style={{fontSize: "1.5rem"}} onClick={handlePause}>PAUSE</button> 
   : 
   null 
}
Enter fullscreen mode Exit fullscreen mode

In other words, while the timer is running, the pause button is rendered.

const handlePause = () => {
    setStart(false)
}
Enter fullscreen mode Exit fullscreen mode

As you can see, handlePause sets start to false which makes our pause button disappear (null is rendered) and our start button is rendered in its place.

The state of start has changed from true to false, triggering our first useEffect (remember to ignore pauseTimer.current for now):

 useEffect(() => {
      if (start === true) {
        pauseTimer.current = counter > 0 && setTimeout(() => setCounter(counter - 1), 1000)
      }
      return () => {
        clearTimeout(pauseTimer.current)
      }
  }, [start, counter, setCounter])
Enter fullscreen mode Exit fullscreen mode

When the user hits PAUSE, useEffect checks to see if start === true (which it doesn't anymore) but the setTimeout from the previous render is still running until our useEffect determines that in fact start does NOT equal true will not run another setTimeout. But the delay happens because the prior setTimeout will complete its run. By then it is often too late and another second has passed.

Want to see this behavior in action? Open the Timer CodeSandbox and delete pauseTimer.current = from line 19, run the timer and try to pause it a few times. You will notice the timer not pausing immediately.

Now that we understand the problem, we can fix it!

Enter the useRef hook to save the day!

Part Three - useRef

Understanding useRef might take some time. I know it did for me. First let's see what the React docs have to say:

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.

Okay, say what?

If you are not sure what any of that means, you are not alone!

I found this blog post written by Lee Warrick very helpful, particularly his explanation for useRef:

Refs exist outside of the re-render cycle.
Think of refs as a variable you’re setting to the side. When your component re-runs it happily skips over that ref until you call it somewhere with .current.

Lightbulb lighting up over man's head

That was my lightbulb moment. A ref is a variable you can define based on an object in state, which will not be affected even when state changes. It will hold its value until you tell it to do something else!

Let's see it in action in our Timer app.

Add useRef to our React import:

import React, { useState, useEffect, useRef } from "react";
Enter fullscreen mode Exit fullscreen mode

From the docs:

const refContainer = useRef(initialValue);

Defining an instance of an object to "reference" later.

Ours looks like:

const pauseTimer = useRef(null)
Enter fullscreen mode Exit fullscreen mode

Make sure to give it a meaningful name, especially if you're using multiple useRefs. Mine is pauseTimer because that is what I want it to do when called. null is my intial value inside useRef() because it doesn't really matter what the initial state of pauseTimer is in my function. We only care what the reference to pauseTimer is once the timer starts ticking down.

pauseTimer is an object with a property of current. EVERY ref created by useRef will be an object with a property of current. pauseTimer.current will be a value which we can set.

Let's take a look at our useEffect one more time, now paying special attention to pauseTimer.current. Here we are setting our conditional (is counter greater than 0?) setTimeout as the value to pauseTimer.current. This gives us access to the value of setTimeout anywhere!

useEffect(() => {
   if (start === true) {
     pauseTimer.current = counter > 0 && setTimeout(() => 
   setCounter(counter - 1), 1000)
   }
   return () => {
     clearTimeout(pauseTimer.current)
   }
}, [start, counter, setCounter])
Enter fullscreen mode Exit fullscreen mode

From here it's pretty straight forward. When the user selects PAUSE now, start updates to false and the useEffect can't run the setTimeout so it runs the clean-up function:

return () => {
     clearTimeout(pauseTimer.current)
}
Enter fullscreen mode Exit fullscreen mode

If we didn't have pauseTimer.current inside our clearTimeout, the timer would continue to tick for another second, just as before because our setTimeout inside the conditional block if (start === true) will run its full course even if we set start to false a second before.

BUT! Since we have pauseTimer.current (a reference to our current setTimeout value) inside clearTimeout, useEffect will skip over if (start === true) and immediately run its cleanup function and stop our setTimeout in its tracks!

And that's the power of useRef! Ability to access a reference to a value anywhere (you can even pass them down from parent to child!) and those references won't change until you tell it to (like we do with our timer every second it updates).

Bonus

This is just the tip of the useRef iceberg. You might be more familiar with useRef and interacting with DOM elements.

In my portfolio website, useRef dictates how I open and close my animated navigation screen.

Inside my component function SideNavBar:

I define my ref

const navRef = useRef()
Enter fullscreen mode Exit fullscreen mode

Create functions to close and open the navigation

function openNav() {
    navRef.current.style.width = "100%"
}

function closeNav() {
    navRef.current.style.width = "0%"
}
Enter fullscreen mode Exit fullscreen mode

And set the React ref attribute of div to navRef

<div id="mySidenav" className="sidenav" ref={navRef}>
Enter fullscreen mode Exit fullscreen mode

And my CSS file with the sidenav class

.sidenav {
  height: 100%;
  width: 0;
  position: fixed;
  z-index: 2;
  top: 0;
  left: 0;
  background-color: #212121;
  overflow-x: hidden;
  transition: 0.6s;
  padding-top: 5rem;
}
Enter fullscreen mode Exit fullscreen mode

Pretty cool, right?

navRef interacts with the DOM element div className="sidenav" because it has the attribute ref={navRef} and when openNav() is called, navRef.current.style.width gets updated to "100%".

And vice versa when 'closeNav()' is called.

Wrapping up

I hope you enjoyed reading the third installment in my React Hooks Series! If you've made it this far, first

Ron Swanson is really proud of you gif

and second

Fez is surfing, thumbs up and thank you!

I plan to continue this series on React hooks. I might cover different aspects of the same hooks or explore all new hooks. So stay tuned and as always, thank you again. It really means so much to me that ANYONE would read anything I write.

Please leave comments, feedback or corrections. I am SURE that I missed something or maybe explained concepts incorrectly. If you see something, let me know! I am doing this to learn myself.

Until next time...

HAPPY CODING

💖 💪 🙅 🚩
jamesncox
James Cox

Posted on August 20, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related