Understanding the useEffect Hook in React

What is useEffect?

useEffect is a React hook that lets functional components perform actions after rendering. These actions can include fetching data, updating the DOM, setting up timers, or handling events. Before hooks, class components used lifecycle methods like componentDidMount and componentWillUnmount to do this.

With useEffect, all these behaviors can be managed inside a functional component without needing a class.


How to use useEffect

To use useEffect, first import it from React and then call it inside your component:

Example: Fetching Data

import { useEffect, useState } from "react";

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

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

  return (
    <div>
      <h3>Post:</h3>
      <p>{data ? data.title : "Loading..."}</p>
    </div>
  );
}

What happens here?

  • useEffect runs when the component first appears on the screen.
  • The empty [] means it only runs once.
  • The effect fetches data and updates the state with setData.

When does useEffect run?

The second argument of useEffect is called the dependency array. It tells React when to run the effect.

1. Updating Document Title

import { useEffect, useState } from "react";

function TitleUpdater() {
  const [title, setTitle] = useState("React App");

  useEffect(() => {
    document.title = title;
  }, [title]);

  return (
    <input 
      type="text" 
      value={title} 
      onChange={(e) => setTitle(e.target.value)} 
      placeholder="Update page title" 
    />
  );
}
  • The document title updates every time title changes.

2. Auto-Saving Form Input

import { useEffect, useState } from "react";

function AutoSaveForm() {
  const [input, setInput] = useState("");

  useEffect(() => {
    const timeout = setTimeout(() => {
      console.log("Auto-saving input:", input);
    }, 1000);

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

  return (
    <input 
      type="text" 
      value={input} 
      onChange={(e) => setInput(e.target.value)} 
      placeholder="Type something..." 
    />
  );
}
  • Waits 1 second after typing to “auto-save” (simulated with console.log).

Cleaning up Effects

Some effects need cleanup, like event listeners and timers. We return a function inside useEffect to clean up.

Example: Handling Window Resize

useEffect(() => {
  const handleResize = () => {
    document.body.style.backgroundColor = window.innerWidth > 800 ? "lightblue" : "lightcoral";
  };

  window.addEventListener("resize", handleResize);
  handleResize();

  return () => {
    window.removeEventListener("resize", handleResize);
  };
}, []);
  • Dynamically changes the background color based on window width.

Example: Pausing Background Music

useEffect(() => {
  const audio = new Audio("/background-music.mp3");
  audio.play();

  return () => {
    audio.pause();
  };
}, []);
  • Plays background music when mounted and pauses when unmounted.

Common Mistakes with useEffect

1. Forgetting Dependencies

useEffect(() => {
  fetchData();
}, []); // fetchData is missing in dependencies!

If fetchData depends on props or state, it should be in the dependency array.

2. Causing an Infinite Loop

useEffect(() => {
  setCount(count + 1); // Triggers re-render forever!
}, [count]);

This keeps updating count, causing an endless loop.

3. Not Cleaning Up

useEffect(() => {
  window.addEventListener("scroll", handleScroll);
}, []);

If not removed, this event listener stays in memory forever, leading to performance issues.


useEffect vs. useLayoutEffect

  • useEffect runs after the screen updates (good for data fetching and timers).
  • useLayoutEffect runs before the screen updates (good for layout changes).

Example:

useLayoutEffect(() => {
  document.body.style.opacity = "1";
});

Most of the time, useEffect is the better choice.


Summary

  • useEffect handles side effects in functional components.
  • Runs after every render, once on mount, or when dependencies change.
  • Cleanup prevents memory leaks and unnecessary operations.
  • Avoid missing dependencies and infinite loops.