paint-brush
Understanding React's useEffect Hook: A Deep Diveby@lastcallofsummer
2,618 reads
2,618 reads

Understanding React's useEffect Hook: A Deep Dive

by Olga StogovaJuly 31st, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

React has brought significant changes to frontend development, especially with the advent of hooks in version 16.8. Among them, the `useEffect` hook has revolutionized how we handle side effects in our functional components. In this guide, we'll demystify its use cases and how it has made side effects management a breeze compared to the lifecycle methods of class components.
featured image - Understanding React's useEffect Hook: A Deep Dive
Olga Stogova HackerNoon profile picture


React has brought significant changes to front-end development, especially with the advent of hooks in version 16.8. Among them, the useEffect hook has revolutionized how we handle side effects in our functional components.


In this guide, we'll demystify useEffect, its use cases, and how it has made side effects management a breeze compared to the lifecycle methods of class components.


Getting to Know useEffect

The useEffect hook is a built-in React hook that allows us to perform side effects in our functional components. Side effects could be anything ranging from fetching data, subscribing to services, or even directly manipulating the DOM. The beauty of useEffect is its ability to encapsulate the functionalities of componentDidMount, componentDidUpdate, and componentWillUnmount into a single, easy-to-use API.


For those who remember the older class components, here is how these lifecycle methods were used:


class OldComponent extends React.Component {
  componentDidMount() {
    // Executed once after the initial rendering
  }

  componentDidUpdate(prevProps, prevState) {
    // Executed after each update
  }

  componentWillUnmount() {
    // Executed just before the component is unmounted and destroyed
  }

  render() {
    // Component's UI
  }
}


With useEffect, we achieve the same functionalities, but now within a functional component:


function NewComponent() {
  useEffect(() => {
    // Replaces componentDidMount and componentDidUpdate

    return () => {
      // Replaces componentWillUnmount
    };
  });

  // Component's UI
}


The useEffect hook is used as follows:


useEffect(() => {
  // Your side effect code...
}, [dependency]);


The first argument is a function that contains the side effect. The second argument is an array of dependencies; the effect will only rerun if any of these dependencies have changed since the last render. If you provide an empty array ([]), the effect will only run once after the initial render, similar to componentDidMount.

useEffect and Cleanup

useEffect can return a cleanup function, which is run when the component is unmounted or before the component re-runs the effect due to dependency changes. This cleanup is crucial for situations where you need to remove subscriptions or event listeners to avoid memory leaks.


Here's an example of how cleanup can be done:


useEffect(() => {
  const subscription = someService.subscribe();

  return () => {
    // Cleanup action
    subscription.unsubscribe();
  };
}, []);


In this code, someService.subscribe() sets up a subscription when the component mounts. The cleanup function subscription.unsubscribe() runs when the component unmounts, canceling the subscription and preventing potential memory leaks.

Diving Deeper with a Practical Example

To further understand useEffect in action, we'll analyze a practical example in two stages.


Stage 1:

We begin with a simple setup involving two components, App and Child.


import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';

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

  console.log(1);

  useEffect(() => {
    console.log(4);
  }, []);

  return <Child count={count} />;
}

function Child({ count }) {

  useEffect(() => {
    console.log(5);

    return () => {
      console.log(6);
    };
  }, [count]);

  return null;
};

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);


In this code, console.log(1) is run at the initial render of App. The useEffect hook then registers console.log(4). The App component then returns Child with count=1, which triggers the rendering of Child.


Inside Child, useEffect registers console.log(5) as count has changed. This effect is then executed--logging 5 to the console.


After Child finishes rendering, React goes back to App and runs console.log(4) as defined in the useEffect hook.


Here is the console output for this scenario:


1
5
4

Stage 2:

Next, let's add more complexity to our components by including additional effects:


import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';

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

  console.log(1);

  useEffect(() => {
    console.log(2);

    return () => {
      console.log(3);
    };
  }, [count]);

  useEffect(() => {
    console.log(4);

    setCount(count => count + 1);
  }, []);

  return <Child count={count} />;
}

function Child({ count }) {

  useEffect(() => {
    console.log(5);

    return () => {
      console.log(6);
    };
  }, [count]);

  return null;
};

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);


The additional useEffect in App logs 2 to the console and includes a cleanup function logging 3.


After Child finishes rendering, React goes back to App and runs console.log(4), which is followed by an update to count causing a rerender of App and Child.


During the rerender, the cleanup functions for useEffect in App and Child are run, logging 3 and 6 respectively. Then, App and Child are rendered again with the new count, logging 1, 2, and 5 to the console.


Here's the resulting console output for this enhanced scenario:


1
5
2
4
1
3
6
5
2


Wrapping Up

The useEffect hook brings significant simplicity and versatility to handling side effects in functional components. By understanding how and when useEffect is run, you can leverage it to simplify your React components and make your side effect management more efficient.


While useEffect is powerful, it requires a deep understanding of its dependencies and execution order to use effectively. Mastering useEffect can give you a greater level of control over your components and how they interact with your application's data and the outside world.


Whether you're migrating old class components or writing new functional components, understanding useEffect is a crucial part of modern React development.