Understanding useEffect: Side Effects in Functional Components

With the introduction of React Hooks in React 16.8, functional components gained the ability to manage side effects using the useEffect Hook. Previously, handling side effects such as data fetching, subscriptions, and manual DOM manipulations was only possible within class components using lifecycle methods.

The useEffect Hook simplifies this process, allowing developers to handle side effects directly inside functional components. In this post, we’ll explore how useEffect works, how it replaces lifecycle methods like componentDidMountcomponentDidUpdate, and componentWillUnmount, and some practical use cases.


How useEffect Works

useEffect is a Hook that runs side effects in function components. Side effects include things like:

  • Fetching data from an API
  • Manually modifying the DOM
  • Setting up subscriptions or event listeners
  • Running timers or intervals

The basic syntax of useEffect looks like this:

import React, { useEffect } from "react";

function ExampleComponent() {
  useEffect(() => {
    console.log("Effect runs after render");

    return () => {
      console.log("Cleanup function runs before unmounting");
    };
  }, []);

  return <div>Check the console for logs</div>;
}

The first argument to useEffect is a function that runs after the component renders. The optional second argument is an array of dependencies, which determines when the effect should re-run.


Replacing Class Lifecycle Methods with useEffect

In class components, lifecycle methods are used to manage side effects. Here’s how useEffect replaces them.

1. Replacing componentDidMount (Run Effect After First Render)

In class components, componentDidMount is used for tasks like fetching data when a component is first displayed:

class ExampleComponent extends React.Component {
  componentDidMount() {
    console.log("Component mounted");
  }

  render() {
    return <div>Class Component</div>;
  }
}

With useEffect, the same behavior is achieved by providing an empty dependency array ([]), ensuring the effect runs only once after the initial render:

import React, { useEffect } from "react";

function ExampleComponent() {
  useEffect(() => {
    console.log("Component mounted");
  }, []);

  return <div>Function Component</div>;
}

2. Replacing componentDidUpdate (Run Effect When Props or State Change)

In class components, componentDidUpdate runs after a component updates, often used to respond to changes in props or state:

class ExampleComponent extends React.Component {
  componentDidUpdate(prevProps) {
    if (prevProps.value !== this.props.value) {
      console.log("Component updated");
    }
  }

  render() {
    return <div>Value: {this.props.value}</div>;
  }
}

With useEffect, we pass the specific dependency that should trigger the effect when it changes:

import React, { useEffect } from "react";

function ExampleComponent({ value }) {
  useEffect(() => {
    console.log("Component updated");
  }, [value]);

  return <div>Value: {value}</div>;
}

If value changes, the effect runs again.


3. Replacing componentWillUnmount (Cleanup Before Component Unmounts)

In class components, componentWillUnmount is used to clean up side effects like event listeners or timers:

class ExampleComponent extends React.Component {
  componentDidMount() {
    window.addEventListener("resize", this.handleResize);
  }

  componentWillUnmount() {
    window.removeEventListener("resize", this.handleResize);
  }

  handleResize = () => {
    console.log("Window resized");
  };

  render() {
    return <div>Resize the window</div>;
  }
}

With useEffect, return a function inside the effect to handle cleanup:

import React, { useEffect } from "react";

function ExampleComponent() {
  useEffect(() => {
    const handleResize = () => {
      console.log("Window resized");
    };

    window.addEventListener("resize", handleResize);

    return () => {
      window.removeEventListener("resize", handleResize);
    };
  }, []);

  return <div>Resize the window</div>;
}

The cleanup function ensures that event listeners are removed when the component is unmounted.


Practical Use Cases of useEffect

1. Fetching Data from an API

Fetching data is a common side effect in React applications. Here’s how useEffect handles an API request:

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

function DataFetcher() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/posts/1")
      .then((response) => response.json())
      .then((json) => setData(json));
  }, []);

  return <div>{data ? data.title : "Loading..."}</div>;
}

Since we provide an empty dependency array ([]), the API call runs only once when the component mounts.


2. Event Listeners

Event listeners often require cleanup to avoid memory leaks. Here’s an example using a keydown listener:

import React, { useEffect } from "react";

function KeyLogger() {
  useEffect(() => {
    const handleKeyPress = (event) => {
      console.log(`Key pressed: ${event.key}`);
    };

    window.addEventListener("keydown", handleKeyPress);

    return () => {
      window.removeEventListener("keydown", handleKeyPress);
    };
  }, []);

  return <div>Press any key and check the console</div>;
}

The cleanup function ensures the event listener is removed when the component unmounts.


3. Setting and Cleaning Up Intervals

If your component needs to run an interval, you should always clear it when the component unmounts to prevent unwanted behavior:

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

function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds((prev) => prev + 1);
    }, 1000);

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

  return <div>Timer: {seconds} seconds</div>;
}

Conclusion

The useEffect Hook provides a powerful and flexible way to manage side effects in functional components, replacing the need for lifecycle methods in class components.

Summary of Lifecycle Replacements

  • componentDidMount → Use useEffect with an empty dependency array [].
  • componentDidUpdate → Use useEffect with specific dependencies.
  • componentWillUnmount → Use useEffect with a cleanup function.

By understanding and applying useEffect correctly, you can simplify your React components while maintaining clean and efficient code.

If you’re new to Hooks, you may also want to explore:

  • How useState simplifies state management
  • Using useReducer for complex state logic
  • Creating custom Hooks for reusable functionality

Hooks are changing the way we write React applications, and this is just the beginning.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *