Understanding the React useEffect() Hook

Subscribe to my newsletter and never miss my upcoming articles

Introduction

In the previous post, we looked at the useState() Hook that adds state to a functional component. We learned how to initialize, update and access state variables in a functional component using useState() Hook.

In this post, we'll focus on the useEffect() Hook that let us perform side effects in functional components. We'll also understand how this particular hook can be used to mimic the behavior of componentDidMount(), componentWillUnmount() and componentDidUpdate() lifecycle methods.

Prerequisites

useEffect() Hook

The operations such as data fetching, manual DOM mutations, logging, setting up a subscription and unsubscribing it are all examples of side effects. These side effects are too early to be dealt while the component is being rendered to the screen. Therefore, the class components are provided with lifecycle methods such as componentDidMount, componentDidUpdate and componentWillUnmount that run after React has updated the DOM.

However, functional components don't have such lifecycle methods. Thus, useEffect Hook was introduced that let us perform side effects in functional components.

The syntax for useEffect Hook is as below:

useEffect(function, [dependencies]);

// first argument is a function where we pass our side effect
// second argument is a dependencies array. it is an optional argument
// with no dependencies array present, useEffect runs after every render

Now that we are clear with the syntax, lets take a look at the following class based component that logs a message to the console after the component is rendered to the screen and on any state update

import React, { Component } from "react";

export default class App extends Component {

  constructor(props) {
    super(props);
    this.state = {
      age: 26
    }
  }

  componentDidMount() {
    console.log(`I am ${this.state.age} years old`);
  }

  componentDidUpdate() {
    console.log(`I am ${this.state.age} years old`);
  }

  render() {
    return (
      <div>
        <p>I am {this.state.age} years old</p>
        <button onClick={() => this.setState({
          age: this.state.age + 1
        })}>Celebrate Birthday</button>
      </div>
    );
  }
}

As per the above code block, after the component gets rendered to the screen, componentDidMount gets called which logs a message to the console. When the button is clicked, the component re-renders with the updated age value and componentDidUpdate gets called which logs a message to the console.

It is evident from the above code block that duplicate code is used in both the lifecycle methods. This is because in many cases you want to perform the same side effect regardless of whether the component was just mounted or has been updated. React class components don't have a lifecycle method that allows a particular code to run after every render.

Now, lets take a look at the functional component using useEffect Hook to achieve the same thing

import React, { useState, useEffect } from "react";

export default function App() {

  const [age, setAge] = useState(26);

  useEffect(() => {
    console.log(`I am ${age} years old`);
  });

  return (
    <div>
      <p>I am {age} years old</p>
      <button onClick={() => setAge(age + 1)}>Celebrate Birthday</button>
    </div>
  );
}

The above code performs the same thing that the class component does but with lesser code. Here, we use the useState Hook to initialize and update the age variable.

Now, lets understand the useEffect Hook

  • In order to perform side effects in functional component, you first need to import useEffect Hook from React.

  • In the App component above, you can see that the State Hook is used to initialize the age variable.

  • useEffect Hook is defined after the State Hook and a function to log the age variable is passed to it.

  • The Effect Hook is defined inside the component so that it can easily access the age variable or any props passed to the component.

  • After React renders the component to the screen, it moves to the useEffect Hook and runs it which logs the age variable to the console.

  • When you click the button, age variable is updated that leads to re-rendering of the component with the updated value. This triggers the effect to run again.

  • The useEffect Hook runs both after the first render and after every update (on state variable change and props change) because there is no dependencies array present as the second argument.

Thus, you can see that the functional component with useEffect Hook is able to achieve the same thing in a single code block which class component achieved in two lifecycle methods.

Now, you got a basic understanding of how useEffect Hook runs. But, without the dependencies array, it is seen that the Effect Hook runs after every render.

There are cases where we don't want it to run after every render as it can lead to undesirable results or performance issues in many cases. In such scenarios, you can make use of dependencies array to determine when useEffect should run again once it has run after the first render. Any change in value of dependencies present in the array triggers the useEffect Hook to run again.

useEffect() with dependencies array

The following code block introduces the dependencies array in the Effect Hook

import React, { useState, useEffect } from "react";

export default function App() {

  const [age, setAge] = useState(26);
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log(`I am ${age} years old`);
  }, [age]);

  return (
    <div>
      <p>I am {age} years old</p>
      <button onClick={() => setAge(age + 1)}>Celebrate Birthday</button>
      <p>The guest count is {count}</p>
      <button onClick={() => setCount(count + 1)}>Add Guest</button>
    </div>
  );
}

Here, we are having two state variables age and count. The dependencies array has age variable present in it. So, once the Effect hook runs after the first render, it will now run only when the age variable is updated. Therefore, if you click the button that updates the count variable, it will not trigger the effect to run. But when the button that updates the age variable is clicked, the effect will run. Therefore, the effect now runs only when age is updated and not after every render.

So far, you've looked at the side effects without cleanup. But there are certain side effects which do require clean up. Some examples include setting up a subscription to some external data source which also needs to be cleaned up so that no memory leak is introduced or setting up a timer and then clearing it after that component is destroyed.

useEffect() with cleanup

Now, lets take a look at the class component where setting up a timer is typically done in componentDidMount method and clean up is done in componentWillUnmount method

import React, { Component } from "react";

export default class App extends Component {

  constructor(props) {
    super(props);
    this.state = {
      timer: 0
    }
  }

  componentDidMount() {
    this.id = setInterval(() => {
      this.setState({
        timer: this.state.timer + 1
      })
    }, 1000);
  }

  componentWillUnmount() {
    clearInterval(this.id);
  }

  render() {
    return (
      <div>
        <p>Timer: {this.state.timer}</p>
      </div>
    );
  }
}

componentDidMount gets executed after the component is rendered to the screen thereby setting up a timer. This timer continues to run till the component is in scope. If the component is about to be unmounted and destroyed, componentWillUnmount gets executed immediately before unmount and any necessary cleanup is performed such as clearing up the timer in the above example.

Now, lets take a look at an equivalent functional component. The function passed to the useEffect hook can return a function that acts as a clean up script. This script runs when the component is about to be unmounted and before every consecutive run of the Effect hook after the first run.

import React, { useState, useEffect } from "react";

export default function App() {

  const [timer, setTimer] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setTimer(prevValue => prevValue + 1)
    }, 1000);
    return () => {
      // cleanup script
      clearInterval(id)
    }
  },[]);

    return (
      <div>
        <p>Timer: {timer}</p>
      </div>
    );
}

In the above example, the Effect hook returns a cleanup function. Since, the dependencies array is empty, the effect does not depend on any changes in the state value or props value and therefore it never re-runs. It will always have the initial value of state and props.

Since the Effect hook is restricted here to run only once, the clean up script gets executed only when the component is about to be unmounted. Therefore using the Effect Hook in this manner is equivalent to componentDidMount and componentWillUnmount lifecycle methods.

You can have more than one Effect Hook in your component.

Conclusion

In this post, you got an understanding of the useEffect() Hook. You learned its syntax and how it is used to perform side effects in a functional component. You also learned about the dependencies array that restricts the Effect hook to run on every render. You learned how related code gets split between lifecycle methods in class component whereas Hooks in functional component let us split the code based on what it is doing and groups related code together.


Thanks for taking the time to read this post. I hope this post helped you!!😊😃 If you liked it, please share.

It would be great to connect with you on Twitter. Please do share your valuable feedback and suggestions👋

No Comments Yet