The Art of React Hooks: Part 2 - Unleashing the Power of useEffect for Efficient Side Effects Management

The Art of React Hooks: Part 2 - Unleashing the Power of useEffect for Efficient Side Effects Management

Here's the second blog of the React Hooks series to help you understand all the React hooks. I will try to cover all aspects of useEffect in this blog, and related concepts too. I will also try to explain how you can use useEffect in .tsx file. So, let's start.

You can check out my previous blog on useState hook here:

https://swapn652.hashnode.dev/the-art-of-react-hooks-part-1-mastering-usestate-for-effortless-state-management

As a React developer, you need a way to handle side effects, that usually involves tasks like: fetching data, DOM manipulation etc. This is where useEffect comes into play.

Now, if you know about class components in React, we have a way to handle side effects there due to various lifecycle methods like:

  1. componentDidMount:

    1. In a class component, componentDidMount is invoked immediately after the component is mounted in the DOM.

    2. It is commonly used for tasks like data fetching.

    3. In the useEffect hook, the same task can be performed by passing an empty dependency array: []. We will talk about this dependency array a bit later.

  2. componentDidUpdate:

    1. This method is called whenever the component updates, either due to changes in props or state.

    2. It allows you to perform side effects based on these updates.

    3. To create a similar effect using the useEffect hook, some dependency variables can be passed in the dependency array.

  3. componentWillUnmount:

    1. This method is invoked just before the component is unmounted and removed from the DOM.

    2. It provides an opportunity to clean up any resources.

    3. The Effect Cleanup return function is used in useEffect to do the same task.


State vs Side Effects

State in React represents the internal data that determines the UI and can be updated using the useState hook. When state changes, React re-renders the component to reflect the updated UI.

Side effects are operations that occur in response to events, like fetching data from an API or interacting with the DOM. They are asynchronous and can cause changes outside of the component's state.


Syntax of useEffect hook

A useEffect hook typically looks like:

useEffect(() => {
    //some code here
}, [])

It accepts two arguments:

  1. Arrow Function: This function contains the actual code for the side effect that you want to perform, like fetching data.

  2. Dependency Array: It defines the dependencies of the effect, i.e., the variables, upon whose change, the application gets re-rendered.


Let's understand a bit about Dependency Array:

  • []

useEffect(() => {
    //some code here
}, [])

When the dependency array is empty like this, it means that it will run only on the first render. This is usually used in cases like when you have an application for which you want to fetch the data only once.

For example:

Let's say you are creating an application that allows users to save their favourite books, and mark them as 'read' or 'want to read'. In this case, for fetching the books' data, you need to run the useEffect hook only for the first time, as data regarding books isn't something that would change in real-time or based on any props or state.


  • No dependency array

You can also declare a useEffect hook without any dependency array. In this case the useEffect hook will run on every application re-render, including the initial render.

useEffect(() => {
    //some code here
})

Note that it's risky to omit the dependency array as it would lead to unnecessary re-rendering.

For example:

Let's say you have an application that includes a real-time chat feature. You want to display the list of online users whenever a user joins or leaves the chat. In this case, you would want to update the list of online users on every render, including the initial render.


  • Some props or state variables passed in the dependency array [state, prop]

      useEffect(() => {
          //some code here
      }, [state, prop])
    

    In this case, the useEffect hook will re-run whenever the passed state and prop would change. The hook will run after the initial render too.

For Example:

Let's say you want to add a switch to your application that allows you to switch from light mode to dark mode and vice versa. In this case, if your application has various components, then it might happen that upon moving to a different component under another tab, the mode won't change. So, to avoid such an error, you can create useEffect hooks with the required dependencies related to that component so that the theme switching works as intended.


Effect Cleanup

Effect Cleanup refers to the process of cleaning up or performing necessary actions when a useEffect hook is unmounted or before it re-runs due to dependency changes.

In certain scenarios, an effect may involve operations like event listeners, subscriptions, or timers that need to be cleaned up when the component is unmounted or before the effect is re-run.

This is usually required to reduce memory leaks.

This is done by including a return function at the end of useEffect hook.

The syntax looks something like:

useEffect(() => {
    // Effect logic here

    return () => {
        // Cleanup logic here
    };
}, []);

Don't worry if you didn't understand it. We will see a working example now.


Working Example of useEffect hook

Follow the following steps to create a React application:

  • Step 1:

    Create a folder and open it in VS Code or any other code editor of your choice.

  • Step 2:

    Open the terminal in the same directory and type the command: yarn create vite

  • Step 3:

    Next, run the command yarn, to install the required packages, and then yarn dev, to run the application.

  • Step 4:

    Go to http://localhost:5143, or the link generated in your terminal and you will see the following page in your browser:

    • Step 5:

      Now, let's start building our application.


  • The empty dependency array []

    Step 1: Create a folder named components.

    Step 2: Create a file named EmptyDependencyArray.jsx.

Step 3: In the EmptyDependencyArray.jsx file, write the following code:

import { useEffect, useState } from 'react';

const EmptyDependencyArray = () => {
  const [count, setCount] = useState(0);
  const [message, setMessage] = useState('');

  useEffect(() => {
    setMessage(`${count+1}. Component mounted`);

    return () => {
      setMessage('Component unmounted');
    };
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <p>{message}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

export default EmptyDependencyArray

Step 4: Call this component in App.jsx

Code Explanation:

In this code, I have create a state variable count, and an associated button that increases the value of the count variable by one on every click.

Then, I have declared a useEffect hook, with and empty dependency array [], to show a message. The message displays the count variable. And it shows how many times the application has been re-rendered. But in this case, since the useEffect hook has an empty dependency array, the useEffect hook will run only once, i.e., initially.

Note, how even on changing the value of variable count, the render message still shows 1.


  • No Dependency Array

    Now, let's look at our next example, i.e., the useEffect hook without any dependency array.

Step 1: Create a file named NoDependencyArray.jsx in the components folder.

Step 2: In that file, add the code:

import { useEffect, useState } from 'react';

const NoDependencyArray = () => {
  const [count, setCount] = useState(0);
  const [message, setMessage] = useState('');

  useEffect(() => {
    setMessage(`${count + 1}. Component re-rendered`);
  });

  return (
    <div>
      <p>Count: {count}</p>
      <p>{message}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

export default NoDependencyArray

Step 3: Call this NoDependencyArray component in the App.jsx file.

Code Explanation

In this code, I have created a state variable count, along with a button, that upon being clicked increases the value of count variable by 1.

Then, I have created a useEffect hook that displays the count variable. Since, no dependency array has been passed, this message gets changed every time, the count variable is updated, thus showing re-rendering every time based on the change of a state.


  • Some props or state variables passed in the dependency array [state, prop]

    Step 1: Create a file named Dependency.jsx in the components folder.

Step 2: Include the following code in that file:

import { useEffect, useState } from 'react';

const Dependency = () => {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('John');
  const [message, setMessage] = useState('');
  const [nameMessage, setNameMessage] = useState(name)

  useEffect(() => {
    setNameMessage(`${name} changed`)
    setMessage(`${count + 1}. Count changed`);
  }, [name]);

  const changeName = () => {
    if (name === 'John') {
      setName('Jane');
    } else {
      setName('John');
    }
  };

  return (
    <div>
      <p>Count: {count}</p>
      <p>Name: {name}</p>
      <p>{message}</p>
      <p>{nameMessage}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={changeName}>Change Name</button>
    </div>
  );
};

export default Dependency

Step 3: Call this component in App.jsx.

Code Explanation

In this code, I have created two variables: count and name, and I have created a useEffect hook that re-renders whenever the name variable gets changed, since the name variable is passed in the dependency array. So, the name gets displayed with every change. But, the count variable doesn't change, as it hasn't been passed in the dependency array.

Note, how even on increasing the value of count variable, it doesn't change below:

But, then on clicking the Change Name button, note how the 1. Count changed gets changed to 11. Count changed. It's because the entire application gets re-rendered based upon the name variable change.


  • Effect Cleanup example

    Step 1: Create a file named Cleanup.jsx in the components folder and include the following code in it:

import { useEffect, useState } from 'react';

const Cleanup = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    // This effect runs after every render
    console.log('Effect triggered');

    // Effect cleanup function
    return () => {
      console.log('Effect cleanup');
    };
  });

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

export default Cleanup

Code Explanation

In this example, the useEffect hook is used without a dependency array, so it will run after every render of the component, including the initial render. When the component mounts, the effect is triggered, and the message Effect triggered is logged to the console.

Now, when the component re-renders due to state changes, the effect is triggered again, and the cleanup function from the previous effect is called before the new effect runs. In this case, the cleanup function simply logs Effect cleanup to the console.

Notice the console in the right.


useEffect in TypeScript

useEffect usage in TypeScript isn't any different. Since, there isn't any data type associated with the useEffect hook, it's of no problem. Let's consider a previous example, of the Empty Dependency Array:

import { useEffect, useState } from 'react';

const EmptyDependencyArray = () => {
  const [count, setCount] = useState(0);
  const [message, setMessage] = useState('');

  useEffect(() => {
    setMessage(`${count+1}. Component mounted`);

    return () => {
      setMessage('Component unmounted');
    };
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <p>{message}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

export default EmptyDependencyArray

If you want to write the same code in .tsx, the only change that would occur would be about the useState hook, not about the useEffect hook. The code would look something like:

import { useEffect, useState } from 'react';

const EmptyDependencyArray = () => {
  const [count, setCount] = useState<number>(0);
  const [message, setMessage] = useState<string>('');

  useEffect(() => {
    setMessage(`${count+1}. Component mounted`);

    return () => {
      setMessage('Component unmounted');
    };
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <p>{message}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

export default EmptyDependencyArray

Note, how only 2 lines related to the declaration of useState changed.

 const [count, setCount] = useState<number>(0);
 const [message, setMessage] = useState<string>('');

That's everything regarding the useEffect hook. I tried to cover almost everything related to this hook. Comment if you think I missed something. Hope this blog was helpful in some way. You can reach out to me on:

  1. Twitter

  2. Showwcase


The other blogs in this series include:

  1. The Art of React Hooks: Part 1 - Mastering useState for Effortless State Management

Did you find this article valuable?

Support Swapnil Pant by becoming a sponsor. Any amount is appreciated!