React Hooks (Part-2) — useEffect

React Hooks (Part-2) — useEffect

The useEffect hook is one of the most powerful and versatile tools in the React developer's toolbox. It allows you to manage side-effects in your functional components and keep your code clean, organized, and performant.

In this blog, we'll take a deep dive into the useEffect hook, covering everything from the basics of how it works to more advanced topics like handling dependencies and cleaning up effects.

It is recommended to read the previous blog on useState before reading this one, as it provides a great foundation for understanding how the useEffect hook works in relation to state. We'll also provide real-world examples and explanations to help you master this essential tool and improve the performance of your React applications.

What are going to cover?

  • Side-effects

  • useEffect (ofcourse)

  • Dependency Array

  • Cleanup functions for useEffects

What are side-effects?

You need to first understand what are the side-effects. We can say that if function is changing some state outside the local environment we can say that its a side-effect.

Examples of such side-effects are,

So, useEffect or just Effect is useful for handling such side-effects. It runs at the end of rendering as its right to to synchronize our component to the outside system.

We use the useEffect as follows:

  useEffect(()=>{

    //Run side effects here

  }, [
    //This is a dependency array
    //This is optional
  ])

Let’s look at one example:

This is a simple counter component, we are incrementing the counter on each button clicked. After each button clicked the useEffect will re-render the component.

const Useeffect = () => {
  const [counter, setCounter] = useState(0);

  useEffect(()=>{
    console.log("inside useEffect")
    //This will rerender after each button click.
  })

  const handleOnClick=()=>{
    console.log("Button Clicked")
    setCounter(counter+1);

  }
  return (
    <div>
      <button onClick={handleOnClick} >Click me!</button>
      <h3>{counter}</h3>
    </div>
  )
}

Now, we want to add a functionality to change the background color of the document in this component only. The code will look like:

Initially, the background color has been set to white. When the user inputs a color, the background color changes. However, even when the counter is incremented, the useEffect hook still runs, and this is because we have not specified a dependency array for the hook.

const Example = () => {
  const [color, setColor] = useState("white");
  const [counter, setCounter] = useState(0);

  useEffect(() => {
    console.log("inside useEffect")
    document.body.style.backgroundColor = color;
  });

  const handleOnClick = (e) => {
    e.preventDefault()
    setCounter(counter+1)
  };
  console.log("rendering")

  return (
    <div>
      <input
        type="text"
        onChange={(e) => {
          setColor(e.target.value);
        }}
        name="color"
        value={color}
      />
      <br style={{margin:20}}/>
      <button onClick={handleOnClick}>Click me!</button>
      <label>{counter}</label>
    </div>
  );
};

What is Dependency Array?

A dependency array is a list of state variables or functions that the useEffect hook is dependent on. Whenever these values change, the useEffect callback function is triggered. By using a dependency array, we can prevent excessive re-rendering of components, making the code more efficient.

useEffect(callback, dependency array)

Now, let’s add a dependency array in above code:

const Example = () => {
  const [color, setColor] = useState("white");
  const [counter, setCounter] = useState(0);

  //This is useEffect for changes the color of document
  useEffect(() => {
    console.log("inside useEffect1")
    document.body.style.backgroundColor = color;
  },[color]);
  //Here in the dependency array we have added the color state varaible
  //Whenever the color state changes then only this useEffect will run

  //In this useEffect we did not provide any depencency array
  useEffect(() => {
    console.log("inside useEffect2")
  });

  const handleOnClick = (e) => {
    e.preventDefault()
    setCounter(counter+1)
  };
  console.log("rendering")

  return (
    <div>
      <input
        type="text"
        onChange={(e) => {
          setColor(e.target.value);
        }}
        name="color"
        value={color}
      />
      <br style={{margin:20}}/>
      <button onClick={handleOnClick}>Click me!</button>
      <label>{counter}</label>
    </div>
  );
};

As, we can see only 2nd useEffect inside the will get invoked when we click on button and 1st useEffect whenever there is change in the input i.e. changes in color state.

But what if the component does not depend on any state, we pass an empty array as dependency array. This will tell useEffect to run only one time. Example of this can be a fetching data from the API.

useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/users")
}, []);

This will fetch data from the API using one call only.

The useEffect Cleanup Function

It’s important to note that the useEffect hook will run on every render by default. For example, if data is being fetched from an API while the user navigates to a different page, the fetching will still occur even though it's no longer needed. We need to abort it. This abort can be done by using AbortController.

As per React documentation, React will perform the clean up as the component gets unmount. But, useEffects 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.

The code will look like:

useEffect(() => {
  effect
  return () => {
    cleanup
  }
}, [input])

Let’s looks at example on how to implement the cleanup function.

  useEffect(() => {
    const controller = new AbortController();
    //AbortController is a simple object 
    //that generates an 'abort' event on its 'signal' property 
    //when the abort() method is called.
    const signal = controller.signal;

    //We can pass this sinal as a option to the 'fetch', 
    //it will listen to it and we can abort the fetch request
     fetch("https://jsonplaceholder.typicode.com/users", {
        signal
      })
      .then((response) => response.json())
      .then((response) => {
        // handle success
        console.log(response);
      })
      .catch((err) => {
        if (err.name === 'AbortError') {
          console.log('successfully aborted');
        } else {
          // handle error
        }
      });
    return () => {
      // cancel the request before component unmounts
      controller.abort();
    };
  }, []);

If we move to different page the abort function will get call before the component gets unmounted. As a result the state will not get updated and will not get error for the same.

So, that is it all about the useEffect. If you haven’t read the useState blog please read it by clicking here. In the next blog we will look into useContext.

Thank you for reading this article and I hope it helped you understand how to effectively use useEffect hook.

Please do follow me on LinkedIn at joshiomkar04.