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
}
In other words, while the timer is running, the pause button is rendered.
const handlePause = () => {
setStart(false)
}
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])
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.
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";
From the docs:
const refContainer = useRef(initialValue);
Defining an instance of an object to "reference" later.
Ours looks like:
const pauseTimer = useRef(null)
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])
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)
}
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()
Create functions to close and open the navigation
function openNav() {
navRef.current.style.width = "100%"
}
function closeNav() {
navRef.current.style.width = "0%"
}
And set the React ref
attribute of div
to navRef
<div id="mySidenav" className="sidenav" ref={navRef}>
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;
}
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
and second
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
Top comments (6)
This was well written, but I often worry about framework overuse.
What I mean specifically is that you don't really need the
useRef
to stop the timer, rather, using theuseEffect
together with a local variable to clear the time out.Perhaps a better use case would've been to show how to have two different effects interact with the same timer, for instance, in your example there can be a
useEffect
handler which has a single responsibility to clear out timers, which might be needed in production a few times, in spite of growing complexity, while another effects take care of starting it.Even better use case are audio/video tags which can be started/paused, or how to deal with stale values.
Good article anyway! Keep them coming!
Thank you for reading and the feedback, Joseph!
Funny that you mention using useEffect to also stop the timer, with a local variable, because I tried that several different ways, and couldn't get the timer to stop immediately on pressing pause. I am sure there is a better way, I just don't know what that is (yet)!
I think for me the issue is having the setTimeout inside the conditional block (start === true) and accessing that variable to clearTimeout isn't possible.
That's why useRef made a lot of sense to me. Accessible basically anywhere!
But I am open to new ideas and solutions! If there is a better way, I would love to know what that is!
Mmm perhaps I am missing something, did you try this?
And we could even get rid of
counter
:Nice Article well explained. But I think even the React Doc's mention to not overuse useref. Didn't played around it much personally so I can't really judge the up and down sides.
That was a very nice explanation. Thank you very much.
Thank you, Yash!