DEV Community

loading...
Cover image for Complete Guide to useEffect Hook in React

Complete Guide to useEffect Hook in React

collegewap
Originally published at codingdeft.com ・7 min read

What is useEffect?

useEffect is a react hook that lets you run side effects inside a functional component. Side effects can be any operation that does not interfere with the main execution of the component, like:

  • Directly manipulating the DOM.
  • Fetching data from an API in the background.
  • Running a function after a certain amount of time using setTimeout or at each interval using setInterval.

The syntax

useEffect has the following syntax:

useEffect(
  () => {
    // the callback function which has the side effect you want to run
    return () => {
      /* this is an optional cleanup callback,
       which will be called before the next render */
    }
  },
  [
    /* this an optional array of dependencies. 
    The useEffect callback runs only when these dependencies change*/
  ]
)
Enter fullscreen mode Exit fullscreen mode

It might look overwhelming at first sight. Do not worry!
In this tutorial, we will break it into pieces and learn all the practical combinations and applications of useEffect.

The simplest useEffect

Since the only mandatory parameter of an useEffect is the callback function, let's write one with just the callback:

import { useEffect, useState } from "react"

function App() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    console.log("Running useEffect")
    document.title = `You clicked ${count} times`
  })

  console.log("Running render")
  return (
    <div className="App">
      <button onClick={() => setCount(count + 1)}>Click Me</button>
    </div>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

In the above example, we are having a button, when clicked will increment the count by 1. Then we have written a useEffect hook where we console log "Running useEffect" and update the title of the page (direct DOM manipulation) with the number of clicks.

If you run the code and open the browser console, you should be able to see the logs as shown below:

Simple UseEffect

As you could see, first the component will be rendered and then the effect will run. Now, if you click on the button, you will see that the component is rendered again (since the state has changed) and the title of the page is updated with the number of clicks.

Simple useEffect clicked

From this, we can infer that the useEffect (with only a callback function) will run after each render.

Infinite Loops

Since useEffect runs after every render, what if the effect inside useEffect causes the component to re-render? That is, if the useEffect updates the state of the component, wouldn't it cause the component to re-render? Wouldn't it cause the useEffect to run again, and so on causing an infinite loop? Yes!

Let's see it using an example:

import { useEffect, useState } from "react"

function App() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    console.log("Running useEffect")
    setCount(count + 1)
  })

  console.log("Running render")
  return (
    <div className="App">
      <button onClick={() => setCount(count + 1)}>Click Me</button>
    </div>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

If you open the console, you will see the code is executed indefinitely:

Infinite loop

If you look closely, React is showing a warning:

Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.

This clearly says that you are updating a state inside the useEffect, which is causing the component to re-render.

How to avoid infinite loops and still update the state inside the useEffect?

This is where the dependency array comes into the picture. We will learn about how to use them in the upcoming sections.

Fetching data with useEffect

Let's build a small app where we fetch the bitcoin price and display it. Before implementing the app, let's add some styles to index.css:

body {
  margin: 10px auto;
  max-width: 800px;
}
.App {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.refresh {
  display: flex;
  align-items: center;
}

.refresh-label {
  margin-right: 10px;
}

.switch {
  position: relative;
  display: inline-block;
  width: 60px;
  height: 34px;
}

.switch input {
  opacity: 0;
  width: 0;
  height: 0;
}

.slider {
  position: absolute;
  cursor: pointer;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: #ccc;
  -webkit-transition: 0.4s;
  transition: 0.4s;
}

.slider:before {
  position: absolute;
  content: "";
  height: 26px;
  width: 26px;
  left: 4px;
  bottom: 4px;
  background-color: white;
  -webkit-transition: 0.4s;
  transition: 0.4s;
}

input:checked + .slider {
  background-color: #2196f3;
}

input:focus + .slider {
  box-shadow: 0 0 1px #2196f3;
}

input:checked + .slider:before {
  -webkit-transform: translateX(26px);
  -ms-transform: translateX(26px);
  transform: translateX(26px);
}

/* Rounded sliders */
.slider.round {
  border-radius: 34px;
}

.slider.round:before {
  border-radius: 50%;
}
Enter fullscreen mode Exit fullscreen mode

We will use the endpoint https://api.coincap.io/v2/assets/bitcoin to fetch the bitcoin price. Now if you are using async-await syntax to fetch the data, your code will look like:

useEffect(async () => {
  try {
    const response = await fetch("https://api.coincap.io/v2/assets/bitcoin")
    const result = await response.json()
    const bitcoinPrice = (+result?.data?.priceUsd).toFixed(2)
    setPrice(bitcoinPrice)
  } catch (error) {
    console.log("error", error)
  }
}, [])
Enter fullscreen mode Exit fullscreen mode

If you use this code, you will get a warning from React telling us not to make useEffect callbacks async. How to tackle this problem? The error message itself suggests having another async function and call it inside the useEffect callback.

useEffect Async Await

So if we update our code accordingly it will look like the following:

import { useEffect, useState } from "react"

function App() {
  const [price, setPrice] = useState()

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch("https://api.coincap.io/v2/assets/bitcoin")
        const result = await response.json()
        const bitcoinPrice = (+result?.data?.priceUsd).toFixed(2)
        setPrice(bitcoinPrice)
      } catch (error) {
        console.log("error", error)
      }
    }
    fetchData()
  }, [])

  return (
    <div className="App">
      <h2>{price && `Bitcoin Price: $${price}`}</h2>
    </div>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

You might observe that we are passing an empty array as a dependency (the second argument to useEffect). This is to ensure that the useEffect runs only once when the component is mounted and not when the component is updated or re-rendered. As you might have guessed correctly, useEffect with an empty dependency array is the same as that of componentDidMount lifecycle method in a class component.

Now if you run the app, you should be able to see the bitcoin price being displayed:

Bitcoin Price

Running it when certain states change

Since the bitcoin price changes every moment, let's make our app more interesting and fetch the price every 5 seconds!

import { useEffect, useState } from "react"

function App() {
  const [price, setPrice] = useState()

  useEffect(() => {
    let interval
    const fetchData = async () => {
      try {
        const response = await fetch("https://api.coincap.io/v2/assets/bitcoin")
        const result = await response.json()
        const bitcoinPrice = (+result?.data?.priceUsd).toFixed(2)
        setPrice(bitcoinPrice)
      } catch (error) {
        console.log("error", error)
      }
    }
    fetchData()

    interval = setInterval(() => {
      fetchData()
    }, 5 * 1000)
    return () => {
      clearInterval(interval)
    }
  }, [])

  return (
    <div className="App">
      <h2>{price && `Bitcoin Price: $${price}`}</h2>
    </div>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

As you may see, we have added a cleanup call back, which will clear the interval, so that it is cleared before the next render and does not run indefinitely and cause memory leakage. You will find more significance to this in the next section.

Now if you run the app and see the network tab, you will see the call happening every 5 seconds and the price getting refreshed:

Fetch Price Every 5 seconds

Let's not stop here, let's add a toggle button to turn off and on the auto refresh:

import { useEffect, useState } from "react"

function App() {
  const [price, setPrice] = useState()
  const [autoRefresh, setAutoRefresh] = useState(true)

  useEffect(() => {
    let interval
    const fetchData = async () => {
      try {
        const response = await fetch("https://api.coincap.io/v2/assets/bitcoin")
        const result = await response.json()
        const bitcoinPrice = (+result?.data?.priceUsd).toFixed(2)
        setPrice(bitcoinPrice)
      } catch (error) {
        console.log("error", error)
      }
    }

    if (!price) {
      // Fetch price for the first time when the app is loaded
      fetchData()
    }

    if (autoRefresh) {
      interval = setInterval(() => {
        fetchData()
      }, 5 * 1000)
    }

    return () => {
      clearInterval(interval)
    }
  }, [autoRefresh, price])

  return (
    <div className="App">
      <h2>{price && `Bitcoin Price: $${price}`}</h2>
      <div className="refresh">
        <div className="refresh-label">Auto refresh:</div>
        <label className="switch">
          <input
            type="checkbox"
            checked={autoRefresh}
            onChange={e => {
              setAutoRefresh(e.target.checked)
            }}
          />
          <span className="slider round"></span>
        </label>
      </div>
    </div>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

As you can see we have added a state called autoRefresh, which will be set to true or false based on the toggle status of the slider. Also, we have added 2 conditions, one to check if the price is present or not and load the price when it is not present.
Another, to check if the autoRefresh is enabled, then only run the logic to fetch price every 5 seconds. Since we need useEffect to be executed every time the value of price and autoRefresh changes, we have added it to the dependency array.

Auto Refresh

The cleanup function will get executed before the next render so that, when we set the autoRefresh to false, the interval will be cleared and data will not be fetched any further.

The difference between the cleanup function and componentWillUnmount is that the cleanup function runs before every re-render and componentWillUnmount runs only when the whole component is unmounted (towards the end of the component lifecycle). You can read more on why they are different here.

General trivia on useEffect

  • useEffect needs to be inside the functional component like any other React hook.
  • One component can have as many useEffect as required. React will make sure that they are clubbed together and executed (wherever possible).
  • Like how state variables can be part of the dependency array, you can have the props as well in the dependency array. Make sure that you are adding only the required dependencies, adding unnecessary dependencies will cause unwanted execution of the effect.
  • If you miss adding a dependency, react will show a warning to help you avoid bugs: Missing Dependency

Source code and demo

You can download the source code here and
view a demo here.

Discussion (0)