useEffect: Side Effects and Cleanup in React

The useEffect hook is one of the most powerful and essential features in React. It enables functional components to handle side effects like data fetching, subscriptions, DOM manipulation, and timers. However, improper use can lead to performance issues, memory leaks, or even infinite loops.

In this module, we’ll explore useEffect in depth: when to use it, how to control its behavior, how dependency arrays work, and how to manage cleanup.


Table of Contents

  1. What Are Side Effects in React?
  2. Introduction to useEffect
  3. Basic Syntax of useEffect
  4. Dependency Array Explained
  5. Common Use Cases for useEffect
  6. Cleanup Functions in useEffect
  7. Avoiding Infinite Loops
  8. Best Practices
  9. Code Examples
  10. Conclusion

1. What Are Side Effects in React?

Side effects are operations that affect things outside the scope of the component’s render cycle. These include:

  • Fetching data from APIs
  • Manipulating the DOM
  • Setting up subscriptions or event listeners
  • Working with timers (e.g., setInterval)
  • Interacting with browser storage (localStorage, sessionStorage)

React is declarative and predictable, so side effects are intentionally isolated using useEffect.


2. Introduction to useEffect

React’s useEffect hook lets you perform side effects in functional components. It essentially replaces the behavior of lifecycle methods like componentDidMount, componentDidUpdate, and componentWillUnmount in class components.


3. Basic Syntax of useEffect

jsxCopyEditimport { useEffect } from 'react';

useEffect(() => {
  // Side effect code here
});

By default, useEffect runs after every render. But its behavior can be customized using a second argument: the dependency array.


4. Dependency Array Explained

jsxCopyEdituseEffect(() => {
  // Side effect
}, [dependency1, dependency2]);
  • [] — empty array: run only once (on mount)
  • [value] — run only when value changes
  • No array: run after every render

Example:

jsxCopyEdituseEffect(() => {
  console.log("Component mounted");
}, []);
jsxCopyEdituseEffect(() => {
  console.log(`Count changed: ${count}`);
}, [count]);

5. Common Use Cases for useEffect

  • Fetching Data
jsxCopyEdituseEffect(() => {
  fetch('/api/data')
    .then(res => res.json())
    .then(setData);
}, []);
  • Event Listeners
jsxCopyEdituseEffect(() => {
  const handleResize = () => console.log(window.innerWidth);
  window.addEventListener('resize', handleResize);
  return () => window.removeEventListener('resize', handleResize);
}, []);
  • Timers
jsxCopyEdituseEffect(() => {
  const timer = setTimeout(() => alert("Hello!"), 3000);
  return () => clearTimeout(timer);
}, []);

6. Cleanup Functions in useEffect

If your side effect returns something (a function), React uses it to clean up before the component unmounts or the effect re-runs.

jsxCopyEdituseEffect(() => {
  const intervalId = setInterval(() => {
    console.log('Tick');
  }, 1000);

  return () => {
    clearInterval(intervalId); // cleanup
  };
}, []);

Cleanup is essential when dealing with intervals, subscriptions, or event listeners to avoid memory leaks.


7. Avoiding Infinite Loops

A common pitfall is forgetting to manage dependencies correctly:

jsxCopyEdituseEffect(() => {
  setCount(count + 1); // BAD: causes infinite loop
}, [count]);

Instead, use function updates or state-only changes when needed:

jsxCopyEdituseEffect(() => {
  setCount(prev => prev + 1);
}, []);

8. Best Practices

  • Always include all dependencies your effect uses.
  • Use ESLint rules like react-hooks/exhaustive-deps to detect missing dependencies.
  • Separate unrelated side effects into separate useEffect calls.
  • Avoid modifying state within useEffect without conditions—this can lead to re-render loops.

9. Code Examples

Fetching Data with Cleanup

jsxCopyEditimport { useState, useEffect } from 'react';

function Posts() {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    let isMounted = true;
    fetch('https://jsonplaceholder.typicode.com/posts')
      .then(res => res.json())
      .then(data => {
        if (isMounted) setPosts(data);
      });

    return () => {
      isMounted = false;
    };
  }, []);

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

10. Conclusion

The useEffect hook is indispensable for any React developer working with dynamic behavior, asynchronous data, or cleanup logic. Mastering it enables you to handle lifecycle-like behavior in functional components cleanly and efficiently.