useEffect cleanup function in React.js – Complete Guide

useEffect cleanup function in React.js – Complete Guide

Frontend technology has improved tremendously in the last ten years. It is now not as simple to construct a front-end web application as it was previously. Today, there are several front-end frameworks and libraries accessible. Each library or framework has its own ways of doing things. Specific code pattern violations may damage the functionality of the frontend application or affect performance, resulting in limited accessibility. React also defines some set of rules of how things should be done. The most important one is the component lifecycle. The way we handled component lifecycle changed tremendously with the introduction of the useEffect hook. Especially handling the unmount and update events with the useEffect hook, which we popularly call a cleanup function.

In this article, we will discuss the usage of a cleanup function in our React application. Following this article, we will look at some key React concepts that are important for a better understanding of React architecture. We will also look at a few good coding practices to ensure the application’s reliability and performance.

Understanding the React lifecycle

Every component in React has a lifecycle that consists of three key steps: mounting, updating, and unmounting. React perform these stages sequentially and specify the steps that each component in React takes. Each stage is crucial, which is one of the reasons React is so quick and efficient. Let us go through each of these stages in further detail.

Mounting

Mounting is the process of creating DOM nodes that correspond to Virtual DOM nodes. During the mounting process, React adds a component to the browser DOM that takes the shape of a Virtual DOM node in memory. The mounting allows us to view the component on the screen.

Updating

We may update the component’s state or props during the update phase, which modifies the component’s return or the virtual DOM node corresponding to the component modifications. React compares the new virtual DOM to the old virtual DOM and only changes the browser DOM as needed. We term this process of updating the component and applying the most recent modifications to the browser DOM as rerendering.

Unmounting

Unmounting is the act of removing a component from the browser DOM, similar to mounting, which is the process of adding a component that resides in memory in the form of a Virtual DOM node. When you unmount a component, React removes the DOM node and all of its children nodes from the browser DOM and the Virtual DOM.

The useEffect hook

Hooks are a great addition to React. They allow us to bring in the functionality of the React functional components that earlier we could only do in the React class-based components. The React’s useEffect hook is used as a lifecycle hook to perform side effects on the component when it mounts, updates, and unmounts.

The useEffect hook takes in two arguments: a callback function and a dependency array. Callbacks are used to perform side effects, and dependencies are variables that, when changed, trigger the side effects. Here is how we use the useEffect hook:

useEffect(() => {
  // do something
}, [var1, var2]);Code language: JavaScript (javascript)

There are three cases possible for the dependency array:

  1. If no dependency array is passed, the callback runs on every rerender.
  2. If we have provided an empty dependency array, the callback only runs on the initial render.
  3. If there are variables (or functions) inside the dependency array, the callback runs every time the value of the variables (or functions) changes.
useEffect(() => {
  // Run on every rerender
});

useEffect(() => {
  // Run on the initial render
}, []);

useEffect(() => {
  // Run when the values of dependency array changes
}, [var1, var2]);
Code language: JavaScript (javascript)

In the callback, we may create event listeners, make API calls to fetch data from the server, create timers and intervals, etc.

A general approach

Inside the useEffect callback, we may run any kind of side-effects, but it is used mostly to create event listeners, make API calls to fetch data from the server, create timers and intervals, etc. Consider a situation where we are creating timers using the setTimeout function inside the useEffect callback. Also, we have passed some values inside the dependency array. Now, every time we change the value of variables (mostly state variables) inside the dependency array, the component rerenders, and the useEffect callback is invoked. Therefore, a new timer is created. Every time the state changes, the component rerenders, and a new timer is created by the useEffect callback, but the previous timers are not cleared. By doing this, soon, our application will be bloated with hundreds and thousands of timers, and the application might crash.

Here is a small demo of the above-discussed process:

export default function Timeout() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const timeout = setTimeout(function () {
      console.log('Hello from Codedamn');
    }, 1000);

    console.log(timeout);
  }, [count]);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Increase count: {count}
      </button>
    </div>
  );
}
Code language: JavaScript (javascript)

In the above code, we have a count state variable, which stores a count. The count is incremented by clicking a button. When the count changes, the useEffect callback creates a new timer and logs its timeout ID. The timeout will log ‘Hello from Codedamn’ after a 1000 milliseconds (or 1 second) delay. Let’s run the code and observe the output.

Using timeouts without a cleanup function.
Using timeouts without a cleanup function.

We can see that every time we click the button, the timeout ID of the timer is logged. If we spam-click the button, a lot of timers will be created. Soon after 1000 milliseconds, we will see a large number of logs created by the timers on the console.

Multiple timeouts created.
Multiple timeouts were created.

To solve this above issue, we must clear the previously created timers before creating a new one. A simple solution is using what we call cleanup functions.

What are cleanup functions?

As we saw above, using the useEffect leads to many issues, such as redundant timers, event listeners, etc. This issue is neither due to the useEffect hook nor the programming logic. React is doing what we instructed, i.e., creating a new timer or event listener every time we update the component state. Now to solve the issue created, we must also instruct React to clear up the previous timers and event listeners before creating a new one. We do this by returning another function called a cleanup function from the useEffect. This function run after the component unmounts or before running the callback again after a rerender.

It is called a cleanup function because the primary task of this function is to perform a cleanup. The cleanup may include clearing up event listeners, timers, intervals, etc., before creating ones. We can also use it to abort ongoing network requests.

Here is how we define a cleanup function inside the useEffect hook:

useEffect(() => {
  // create timers

  return () => {
    // clear timers
  };
}, []);
Code language: JavaScript (javascript)

In the above code, we have returned a function from the useEffect hook’s callback function. The function is the cleanup function for the particular side effect. Inside the cleanup function, we may write any code that we wish to execute after the component amounts or before running the callback again after a rerender. Mostly, this function will perform some form of cleanup before running the useEffect hook’s callback again.

Using cleanup functions in React

Now that we have understood the basics of the useEffect hook and cleanup functions, we may move forward and learn some applications of the cleanup functions. We’ll mostly discuss about cleaning up event listeners, timers, and intervals, but there are plenty of uses for the cleanup functions, which you’ll learn gradually. Let us now look at various applications of the cleanup functions.

Clearing up timers

We have already discussed the problem that arises when we don’t clear up previously created timers before creating new ones. On every state update, a new timer(s) will be created, and soon the application will crash due to a large number of timers running. Even if the application doesn’t crash, the performance will degrade, which results in poor user experience and accessibility.

To prevent this, we will return a cleanup function from the useEffect hook’s callback function to clear the existing timer before creating a new one. To do this, we will use the timeout ID and the clearTimeout function.

Here is how we set up the cleanup function for clearing the timer correctly:

export default function Timeout() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const timeout = setTimeout(function () {
      console.log('Hello from Codedamn');
    }, 1000);

    return () => {
      clearTimeout(timeout);
    };
  }, [count]);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Increase count: {count}
      </button>
    </div>
  );
}
Code language: JavaScript (javascript)

In the above code, we have a count state variable, which stores a count. The count is incremented by clicking a button. When the count changes, the useEffect callback creates a new timer and logs its timeout ID. But this time, before rerunning the callback, the previous timer will be cleared using the cleanup function returned from the callback. The timeout will log ‘Hello from Codedamn’ after a 1000 milliseconds (or 1 second) delay. Let’s run the code and observe the output:

Only single timeout is created.
Only a single timeout is created.

Even if we spam click now, there won’t be multiple timeouts. Only one timeout is created, and if the button is clicked again before the previous timeout expires, first, the previously created timeout is cleared, and then the new one is created.

Clearing up intervals

Similar to how we cleared the timeouts, we can clear the intervals too. The only difference now will be instead of using a timeout ID and a clearTimeout function, we will use an interval ID and a clearInterval function.

export default function Interval() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const interval = setInterval(function () {
      console.log('Hello from Codedamn!');
    }, 1000);

    return () => {
      clearInterval(interval);
    };
  }, [count]);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Increase count: {count}
      </button>
    </div>
  );
}
Code language: JavaScript (javascript)

Clearing up event listeners

Just like we cleared up the timers and intervals, we will clear the event listeners. To clear an event listener, we will use the removeEventListener function. It accepts two arguments. First is the name of the listener, such as ‘click’, ‘scroll’, etc., and the second is the callback function attached to the event listeners. Here is how we will clear up an event listener:

removeEventListener(EventName, Callback);Code language: JavaScript (javascript)

Now, let’s implement the same inside the useEffect hook’s callback function as a cleanup function:

function clickEventCallback() {
  console.log('Clicked');
}

export default function Timeout() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.body.addEventListener('click', ClickEventCallback);

    return () => {
      document.body.removeEventListener('click', ClickEventCallback);
    };
  }, [count]);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Increase count: {count}
      </button>
    </div>
  );
}
Code language: JavaScript (javascript)

Summary

React defines some set of rules of how things should be done. The most important one is the component lifecycle. The way we handled the component lifecycle changed tremendously with the introduction of the useEffect lifecycle hook. It is used to perform side effects on the component when it mounts, updates, and unmounts. Using the useEffect hook leads to many issues, such as redundant timers, event listners, etc. To solve the issues, we must instruct React to clear up the previous timers and event listeners before creating a new one. We do this by returning another function called a cleanup function from the useEffect. Using cleanup functions, we can clear up event listeners, timers, intervals, etc.

You can find the complete source code using the following link.

If you want to learn more about how React works under the hood, you can check out the following article by Codedamn:

React Internals Explained – How React works under the hood?

This article hopefully provided you with some new information. Share it with your friends if you enjoy it. Also, please provide your feedback in the comments section.

Thank you so much for reading 😄

Sharing is caring

Did you like what Varun Tiwari wrote? Thank them for their work by sharing it on social media.

0/10000

No comments so far