React: Master useEffect Hook.

React: Master useEffect Hook.

A complete guide for absolute beginners.

ยท

9 min read

Featured on daily.dev

Introduction

useEffect() is one of the most used hooks in React applications. This makes it a favorite topic for interviewers to talk about it in Frontend/React interviews. Therefore having a good understanding of useEffect is crucial.

You might be knowing, that we can write React applications either using class-based components or using functional components (using hooks).

In this article, I don't assume you have knowledge of writing class-based components. Though we will see useEffect in context with class-based components in a later section of the article.

Let's Begin!

Use and Syntax

useEffect is used to cause side effects, for example, making an API request, manipulating DOM directly, adding timers (setTimeout or setInterval) etc.

Syntax

useEffect(() => {
       // callback function

       return () => {
          // cleanup
       }
}, [dependencies]);

Let us understand the syntax

The useEffect hook takes a callback function as its first argument and optionally an array of variables ( referred to as dependency array) as the 2nd argument. The variables could be anything, a primitive type or a reference type.

useEffect can optionally return a function too. We will see the significance of all these things involved in the syntax. ( Have some patience) ๐Ÿ˜….

Million dollar question is - When actually useEffect runs?

You have written the code inside usEffect( Inside the useEffect's callback, to be precise), but when does that code run?

The callback function passed to useEffect is executed after every render by default. I said by default, meaning there are ways to control it. We will look into that as well.

Let's take an example and see that useEffect runs after every render.

import { useEffect } from "react";

function App() {
  useEffect(() => {
    console.log("UseEffect fired");
  });

  // This console just before render
  console.log("Just before render");
  return (
    <div>
      <h1>Example - UseEffect</h1>
    </div>
  );
}

export default App;

Start the React app, navigate to the browser, and check the console logs. Screenshot from 2022-05-17 16-46-55.png

Notice that console.log written just before the return statement is printed first and then the console.log written inside the useEffect is printed. This proves our point that useEffect runs after render.

Now, let's add a state variable count and a button to update that count when clicked. You should be knowing that whenever the state or props of a component changes, it is re-rendered.

We are doing this to prove that useEffect runs after every render. ๐Ÿ˜‰

import { useState, useEffect } from "react";

function App() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log("UseEffect fired");
  });

  // This console just before render
  console.log("Just before render");
  return (
    <div>
      <h3> Count - {count}</h3>
      <button onClick={() => setCount(count + 1)}> Click me</button>
    </div>
  );
}

export default App;

Go and test this. When you click the Click me button, it increases the count that causes the app to re-render.

As you can see in the image below, console.log inside useEffect is printed 4 times, the first print is when the app was rendered for the first time, and then 3 more times as I have clicked the button 3 times to increase the count to 3. Screenshot from 2022-05-20 16-08-37.png

So, we can conclude that useEffect runs after every render.

A common mistake beginners make -

Think about you have a boolean state variable and you set the value of that to true/false inside useEffect. If it is true you set it to false and vice-versa. Let's write code to do this.

import { useState, useEffect } from "react";

function App() {
  const [bool, setBool] = useState(true);
  useEffect(() => {
    setBool(!bool);
  });

  // This console just before render
  console.log(bool);
  return (
    <div>
      <h3>Beginners mistake</h3>
    </div>
  );
}

export default App;

Try to think, what would happen if you run this code? It might freeze your browser (frozen mine lol ๐Ÿ˜‚). You can see the error in the image below. This is basically a StackOverflow error, meaning you keep calling useEffect infinitely.

But, how does it happen? Let's see that next. Screenshot from 2022-05-17 17-29-09.png

The reason for this error is that there is an infinite cycle of re-renders in our app.

Identify The Cycle -

Component rendered for the first time -> useEffect runs and changes the state variable -> As state got changed, component re-rendered -> useEffect fired and changed the state variable again -> As state got changed, component re-rendered -> useEffect fired and changed the state variable again -> As state got changed, component re-rendered -> useEffect fired.............so on.

I can keep writing this infinitely ๐Ÿ˜‚.

How can we stop it and make it work properly?

The answer is to make use of the 2nd argument passed to useEffect, the dependency array.

You can put as many variables ( functions, objects, or primitive type variables) as you wish inside that array. Doing so is basically telling React to run useEffect whenever any of the variables mentioned in the dependency array changes.

Let's see a couple of examples.

Example -1

import { useState, useEffect } from "react";

function App() {
  const [count1, setCount1] = useState(10);
  const [count2, setCount2] = useState(20);

  useEffect(() => {
    console.log("Useffect executed");
  }, [count2]);

  return (
    <div>
      <h3>Count1: {count1}</h3>
      <h3>Count2: {count2}</h3>

      <button onClick={() => setCount1(count1 + 1)}>Change counter -1 </button>
      <button onClick={() => setCount2(count2 + 1)}>Change counter -2 </button>
    </div>
  );
}

export default App;

Look at the above code closely. Think about -

  • What would happen when you run this code?
  • What happens when you click the Change counter -1 button?
  • What happens when you click the Change counter -2 button?

In the example-1 above, useEffect runs -

  1. When the component is rendered for the first time. useEffect is always executed after the first render. Nothing interesting here.
  2. useEffect will be fired every time the value of the count2 changes. Why? Because we have included count2 in the dependency array and as I told you useEffect runs whenever any of the variables mentioned in the dependency array changes.

Here is what is happening and why?

  1. When Change counter -1 is clicked, the count1 changes and the component is re-rendered. Although, the component is re-rendered useEffect doesn't execute because the dependency array doesn't include the changed variable count1.
  2. When Change counter -2 is clicked, the count2 changes, and the component is re-rendered. Then, useEffect is executed because the dependency array includes the changed variable count2.

You can play with all the examples here.

Example -2 Now, change our effect to something like this ๐Ÿ‘‡

// Example -2 
  useEffect(() => {
    console.log("Useffect executed", count1, count2);
  }, [count2, count1]);

In this example-2, clicking on any of the buttons changes either count1 or count2 and change in anyone of these two triggers to run useEffect because both are included in the dependency array.

Hope things are making sense to you. If not, reach out to me here on Twitter

Example -3

What if you want your useEffect to run only once? It is simple, just tell React that this useEffect doesn't depend on anything.

How to tell that, by passing an empty array as the 2nd argument to useEffect.

// Example -3
  useEffect(() => {
    console.log("This Useffect executed only once, just after first rended");
  }, [ ]);

Looking at the empty dependency array, React concludes that there is nothing changed in the dependency array so it decides not to execute useEffect.

Multiple useEffect in an Component

Yes, we can have more than one useEffect hook inside our components. This is very useful for separating logic and controlling when to run the useEffect.

For example, think about the situation when we want to run a different piece of code when count1 changes and a completely different piece of code when count2 changes.

 useEffect(() => {
    console.log("Useffect executed after change in count1");
  }, [count1]);

  // // Example -3
  useEffect(() => {
    console.log("Useffect executed after change in count2");
  }, [ count2 ]);

The first useEffect runs whenever count1 changes and the second useEffect runs when count2 changes. Both of the effects run after the first render though.

Data Fetching From an API Endpoint.

As I told you, useEffect is used to cause side effects, for example, making an API request. So, let's make a request to the API. I will be using this https://forkify-api.herokuapp.com/api/get?rId=47746 that sends the details of a dish.

We will store these details in a state variable and show them on our web page.

import { useEffect, useState } from "react";
import "./styles.css";

export default function App() {
  const [loading, setLoading] = useState(true);
  const [dish, setDish] = useState({
    title: "",
    image_url: ""
  });

  useEffect(() => {
    fetch("https://forkify-api.herokuapp.com/api/get?rId=47746")
      .then((res) => res.json())
      .then((dish) => {
        const { title, image_url } = dish.recipe;
        console.log(dish);
        setDish({
          title,
          image_url
        });
        setLoading(false);
      });
  }, []);
  console.log(dish);
  return (
    <div className="App">
      {loading ? (
        <h2>Loading....</h2>
      ) : (
        <div>
          <h1> {dish.title} </h1>
          <img src={dish.image_url} alt="Pizza" />
        </div>
      )}
    </div>
  );
}

Play with the code here

When to return a cleanup function from useEffect.

As discussed in the beginning, we can optionally return a function from useEffect. This returned function is generally termed as cleanup. Let's talk about this.

When we return a function from inside the callback of the useEffect, this function is executed by React just before unmounting the component and also after every re-render. In simple terms, Unmounting means the removal of a component from the user interface.

Read more about why cleanups run after every re-renderhere.

We use this function to do cleanup like removing eventListeners clearing timers using clearInterval clearTimeout. And, doing this is actually necessary and very logical.

Think about if a component is removed why should we let eventListeners attached to it and put the burden on the browser. This can even cause memory leaks. So to avoid this, we make use of the ability to return a cleanup function from inside useEffect.

Let's write some code.

useEffect(() => {
    const id = setTimeout(() => {
      console.log("Settimeout called after 1000 ms")
    });
    // This is cleanup function
    // You can write es6 arrow function too if u wish
    return function(){
      clearTimeout(id)
    }
  }, [ ]);

Explanation: Inside useEffect we have used setTimeout. At the time of unmounting, we should clear this timeout.

To do this, we are returning a function from useEffect. React executes this returned function at the time of unmounting the component. As soon as React executes this function, clearTimeout(id) s expected and this clears the used setTimeout.

Relaton with class-based components.

This section is only for those who have prior knowledge of writing class-based components.

useEffect basically serves the purpose of 3 lifecycle methods used in class-based components -

  1. componentDidMount();
  2. componentDidUpdate();
  3. componentWillUnmount(); Here is how -

  4. When useEffect has an empty dependency array, it is executed only once, after the component mounts. This is same as how componentDidMount() works.

  5. When useEffect has a non-empty dependency array, it is executed every time any of the variable inside the dependency array is updated. This is same as how componentDidUpdatet() works.
  6. React performs the cleanup when the component unmounts. However, as we learned earlier, effects run for every render and not just once.

This is why React also cleans up effects from the previous render before running the effects next time. This serves the same purpose as componentWillUnmount() but it does more than that.

This is all I wanted to share with you in this article. I strongly hope you find this article helpful in your learning journey. If you get any question to ask, please reach out to me on Twitter here.

Don't forget to like this article and give your valuable feedback in the comments. I'm waiting to hear your thoughts on what you have to say about the article.

You can follow me here and on Twitter to learn more in-depth articles on various JavaScript and React topics.

Keep learning. Thank you!

Did you find this article valuable?

Support Faheem Khan by becoming a sponsor. Any amount is appreciated!