Introduction

Memory leaks in React often occur when side effects are not properly cleaned up inside the useEffect hook. This happens with timers, subscriptions, event listeners, or async requests that remain active even after a component unmounts. Over time, these leaks degrade performance and cause UI issues. This guide explains why leaks occur, how to prevent them, and best practices for cleanup in React functional components.

Conceptual Background

React’s useEffect hook runs side effects after a render. Side effects include:

When a component unmounts or re-renders, any unfinished side effects may continue to run. If not cleaned up, they cause memory leaks by holding references to unused state or DOM elements.

React provides a solution: return a cleanup function inside useEffect . This ensures that any active effect is disposed of before the component is unmounted.

Step-by-Step Walkthrough

1. Basic Cleanup with useEffect

  
    import { useEffect } from "react";

function TimerComponent() {
  useEffect(() => {
    const timer = setInterval(() => {
      console.log("Running timer...");
    }, 1000);

    // Cleanup function
    return () => {
      clearInterval(timer);
    };
  }, []);

  return <h2>Timer running…</h2>;
}
  

Explanation

2. Cleaning Event Listeners

  
    import { useEffect } from "react";

function WindowResizeLogger() {
  useEffect(() => {
    const handleResize = () => console.log("Resized to", window.innerWidth);
    window.addEventListener("resize", handleResize);

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

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

Without cleanup, every render adds a new listener, leading to memory leaks.

3. Handling Async Calls in useEffect

Async requests may resolve after a component unmounts. Prevent leaks with an abort controller.

  
    import { useEffect, useState } from "react";

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

  useEffect(() => {
    const controller = new AbortController();

    fetch("https://jsonplaceholder.typicode.com/posts/1", {
      signal: controller.signal,
    })
      .then((res) => res.json())
      .then((json) => setData(json))
      .catch((err) => {
        if (err.name !== "AbortError") {
          console.error("Fetch error:", err);
        }
      });

    return () => controller.abort(); // cancel fetch on unmount
  }, []);

  return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
  

4. Cleaning WebSocket or Subscription

  
    import { useEffect } from "react";

function WebSocketComponent() {
  useEffect(() => {
    const socket = new WebSocket("wss://echo.websocket.org");
    socket.onmessage = (event) => console.log(event.data);

    return () => {
      socket.close();
    };
  }, []);

  return <h2>WebSocket connected</h2>;
}
  

Mermaid Flowchart

  
    flowchart TD
    A[Component Mounts] --> B[useEffect runs]
    B --> C[Side effect starts (timer, fetch, listener, socket)]
    C --> D{Component re-render/unmount?}
    D -->|Yes| E[Cleanup function runs: clear/remove/abort]
    D -->|No| F[Side effect continues]
  

Use Cases / Scenarios

Limitations / Considerations

Fixes for Common Pitfalls

FAQs

Q1: Do I always need a cleanup function in useEffect?

No. Only when the effect creates a subscription, timer, or resource that needs disposal.

Q2: What happens if I forget cleanup?

The side effect continues running in memory, causing leaks and performance issues.

Q3: Should I use async directly in useEffect?

No. Declare an async function inside and call it. Cleanup with AbortController to stop stale requests.

Q4: Does Strict Mode affect useEffect cleanup?

Yes. In development, React mounts and unmounts components twice to detect issues. Always write cleanup defensively.

Q5: Are memory leaks only about useEffect?

No. They can also come from unbounded global variables, poor caching, or unreleased references. But useEffect is the most common culprit.

Conclusion

To prevent memory leaks in React:

By following these practices, your React apps remain performant, stable, and free of hidden memory issues.