DEV Community

Cover image for React Hooks Series: useEffect
James Cox
James Cox

Posted on • Updated on

React Hooks Series: useEffect

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

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

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

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

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

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

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

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

Discussion (2)

Collapse
spic profile image
Sascha Picard

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?

useEffect(() => {
    if (counter === 0) {
      setStart(false)
    }
}, [
    counter, // I get it: we want to run the effect when counter changes   
    setStart  // I dont get it: this value will never change throughout the lifetime of the component
])
Collapse
jamesncox profile image
James Cox Author

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!