Introduction
A reminder that all code examples come directly from the Timer CodeSandbox I put together. You are encouraged to open it, fork it, play around with the code, follow along, whatever helps you learn best!
In my first article in the React Hooks Series I wrote about the useState hook. This iteration will focus on useEffect (my Timer example calls the useRef hook first, but I think it makes more sense to understand what's happening with useEffect before we tackle useRef).
Part Two - useEffect
What is useEffect?
From the React docs: "The Effect Hook lets you perform side effects in function components:"
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
If you’re familiar with React class lifecycle methods, you can think of useEffect Hook as componentDidMount, componentDidUpdate, and componentWillUnmount combined.
Does useEffect run after every render? Yes! By default, it runs both after the first render and after every update.
In my own words: useEffect runs anytime something changes. This could be the user interacting with a form, a button, etc. State changing, like counter
in my Timer app, counting down every second or start
being set from false
to true
when the user hits START. Or the component itself is loaded (mounted) or unloaded (unmounted) from the screen.
Getting started
Add useEffect to our React import.
import React, { useState, useEffect } from "react";
Let's take a look at the first useEffect function.
useEffect(() => {
if (start === true) {
pauseTimer.current = counter > 0 && setTimeout(() => setCounter(counter - 1), 1000)
}
return () => {
clearTimeout(pauseTimer.current)
}
}, [start, counter, setCounter])
A lot going on here. Remember that we set state of start
to false
. Therefore, even if our Timer component updates, this useEffect() will not run until start === true
.
Inside of our if (start === true)
conditional block is the meat and potatoes of our useEffect (and really the whole point of the app!):
pauseTimer.current = counter > 0 && setTimeout(() => setCounter(counter - 1), 1000)
However, we are going to ignore pauseTimer.current
for now (this logic is tied to our PAUSE button and the useRef hook).
Let's examine the following:
When start === true
run the code inside the block:
counter > 0 && setTimeout(() => setCounter(counter - 1), 1000)
If counter > 0
run: setTimeout(() => setCounter(counter - 1), 1000)
(Remember that we use setCounter(input)
to update counter
. Let's say a user selects 10 seconds, input === 10
and when user hits submit, then counter === 10
.)
This is where the magic happens. Counter is 10. setTimeout accepts a function to run and a time in milliseconds. When that time expires, setTimeOut will run the function. In our case setTimeout accepts our setCounter()
function and will run after 1000 milliseconds (1 second). setCounter(counter - 1)
will run after 1 second, changing 10 to 9.
Every single time the state of ANYTHING changes/updates, useEffect is called. Therefore, when counter
changes from 10 to 9, useEffect is called again! Is 9 greater than 0? YES! Then run the code to the right of if counter > 0
which happens to be our setTimeout function. This process happens until our if counter > 0
is no longer true
. When counter === 0
, counter
is no longer greater than 0, or false
and will skip over the setTimeout to the right.
Next, take a look at this.
return () => {
clearTimeout(pauseTimer.current)
}
What is this return function inside our useEffect?
This has to do with cleanup. I had to deal with this in my GIF FIT app (the inspiration for this entire series of React hooks articles), where I am dealing with several setTimeouts (6 in total) running in sync.
They are separate components in my app. When one timer ended, another began. I quickly discovered that if you do not "clean up" certain functions inside a useEffect, you will get something called a "memory leak". Basically, my setTimeouts were still running in the background, taking up memory. NOT GOOD.
Luckily, useEffect has a simple solution. It accepts a final function which can clean up effects from the previous render and when the component finally unmounts. The above function inside our useEffect is effectively killing the setTimeout and avoiding any memory leaks! Cool, huh?
Putting it together
{
start === false && counter !== null && counter !== 0
?
<button style={{fontSize: "1.5rem"}} onClick={handleStart}>START</button>
:
null
}
{
start === true && counter !== 0
?
<button style={{fontSize: "1.5rem"}} onClick={handlePause}>PAUSE</button>
:
null
}
In Part One, useState(), I showed how we rendered the START button if start === false && counter !== null && counter !== 0
Which gives us access to onClick={handleStart}
user clicks start
const handleStart = () => {
setStart(true)
}
start === true
state changes and useEffect() runs
Our setTimeout decrements count
by one
state changes and useEffect runs again
Repeat this action until count === 0
and is no longer greater than 0.
Yay! Our timer is working!
I'm about to blow your mind. Maybe. Did you know you can have multiple useEffect functions in the same component? Once my timer is finished (counter === 0
), I needed a way to reset the state of start
back to false
Enter a second useEffect!
useEffect(() => {
if (counter === 0) {
setStart(false)
}
}, [counter, setStart])
Pretty straight forward. When useEffect detects that counter === 0
it will call setStart(false)
which means start === false
.
This is a good time to talk about what [start, counter, setCounter]
and [counter, setStart]
does at the end of our two useEffects. These are dependencies that we are calling inside our useEffects, and we are explicitly telling our useEffects that when one of these change, do your thing!
You don't always need that array to wrap up a useEffect, but it's a good habit to get into. And if you want a useEffect to run only once, you place an empty array []
at end of your useEffect function, because there aren't any dependencies, it won't know to run when state changes again.
Wrapping up
Thank you for reading Part Two of my React Hooks series. If you missed Part One, please check it out and let me know what you think.
Part Three will focus on the useRef hook and I am really excited about this one. The useRef hook is my least comfortable in terms of use and understanding. But so far it has been one of my favorites to work with. I am really impressed by how much the useRef hook can accomplish.
As always, thank you for making it this far and I look forward to any questions, comments, corrects, and even criticism!
HAPPY CODING
Top comments (2)
Hey James,
thanks for your articles, I love how you take time to explain things!
I am wondering about one thing:
Did you have a particular reason to add the "setters" for the state variables created with setState (I am talking about setStart() and setCounter() ) to the dependencies arrays of both useEffect() calls?
This is a great question, and I want to do some more digging into this and promise to reply again with some more resources that better explain, but the short answer for now is: the linter asks me to put in those dependencies.
I guess you could argue that you don’t have to. But according to the docs “it’s totally free”. And for me I don’t like having linter warnings.
I will see if I can dig up any other better explanations and resources. Maybe someone reading this will have a better answer too. Thanks for asking and I will get back to you soon!