Memory Leak in React

Memory leaks in React are a common interview topic because they affect performance, memory usage, and application stability. A memory leak occurs when objects that are no longer needed remain in memory because something still references them.

What is a Memory Leak?

A memory leak happens when memory allocated for an object cannot be released because it is still being referenced, even though it is no longer needed.

Example

Imagine a user visits a page.

  • React creates components.
  • Components allocate memory.
  • User navigates away.
  • Component is removed from the UI.

If some timer, event listener, or API request is still running and holding a reference to the component, JavaScript cannot garbage collect it.

Result:

  • Increasing memory usage
  • Slow application
  • High CPU usage
  • Browser crashes (large applications)

How Memory Normally Works

Lifecycle StageWithout Memory LeakWith Memory Leak
Component MountReact creates the component and allocates memory.React creates the component and allocates memory.
Side EffectsResources (timer, listener, API, WebSocket) are created.Resources (timer, listener, API, WebSocket) are created.
User Navigates AwayComponent unmounts.Component unmounts.
CleanupCleanup function removes all resources.Cleanup function is missing or incomplete.
ReferencesNo references remain.Active references still exist.
Garbage CollectionJavaScript Garbage Collector releases memory.Garbage Collector cannot release memory because references still exist.
Final ResultStable memory usage and good performance.Increasing memory usage, slower application, and potential browser crashes.

Common Memory Leaks in React

Memory LeakProblemSolution
Timer (setInterval)Continues running after component unmountclearInterval()
Event ListenerListener remains attachedremoveEventListener()
Fetch RequestResponse arrives after component unmountAbortController.abort()
WebSocketConnection remains opensocket.close()
Subscription (RxJS)Subscription continues receiving updatesunsubscribe()
Async OperationUpdates state after component unmountCancel request or ignore result
Large ClosureLarge objects stay referencedRemove unnecessary references

1. Timer (setInterval)

Problem

A timer continues executing even after the component has been removed from the DOM.

Not Recommended

useEffect(() => {
  const id = setInterval(() => {
    console.log("Running...");
  }, 1000);
}, []);

Why is this bad?

  • Interval never stops.
  • JavaScript still keeps a reference.
  • Memory cannot be reclaimed.

✅ Recommended

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

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

Why is this recommended?

  • Interval is cleared during cleanup.
  • No unnecessary work continues.
  • Memory can be released.

2. Event Listener

Problem

Every mount adds a new event listener.

Not Recommended

useEffect(() => {
  window.addEventListener("resize", handleResize);
}, []);

Why is this bad?

  • Listener remains attached.
  • Multiple listeners accumulate.
  • Causes memory leaks and duplicate executions.

✅ Recommended

useEffect(() => {
  window.addEventListener("resize", handleResize);

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

Why is this recommended?

  • Removes listener when component unmounts.
  • Prevents duplicate listeners.
  • Frees memory.

3. Fetch API Request

Problem

The API request completes after the component has already unmounted.

Not Recommended

useEffect(() => {
  fetch("/users")
    .then(res => res.json())
    .then(setUsers);
}, []);

Why is this bad?

  • Request continues running.
  • May try to update state after unmount.
  • Wastes network and memory resources.

✅ Recommended

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

  fetch("/users", {
    signal: controller.signal
  })
    .then(res => res.json())
    .then(setUsers);

  return () => {
    controller.abort();
  };
}, []);

Why is this recommended?

  • Cancels pending request.
  • Prevents unnecessary work.
  • Avoids state updates after unmount.

4. WebSocket

Problem

Connection stays open forever.

Not Recommended

useEffect(() => {
  const socket = new WebSocket(url);
}, []);

Why is this bad?

  • Open connection consumes memory.
  • Browser keeps network resources allocated.

✅ Recommended

useEffect(() => {
  const socket = new WebSocket(url);

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

Why is this recommended?

  • Closes the connection.
  • Releases network resources.
  • Prevents memory leaks.

5. RxJS Subscription

Problem

Subscription continues after component removal.

Not Recommended

useEffect(() => {
  observable.subscribe(data => {
    setData(data);
  });
}, []);

Why is this bad?

  • Component keeps receiving updates.
  • Memory usage keeps growing.

✅ Recommended

useEffect(() => {
  const subscription = observable.subscribe(data => {
    setData(data);
  });

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

Why is this recommended?

  • Stops receiving updates.
  • Releases subscription resources.

6. Async Operation

Problem

Async function finishes after component unmount.

Not Recommended

useEffect(() => {
  async function load() {
    const data = await fetchData();
    setData(data);
  }

  load();
}, []);

Why is this bad?

  • State update occurs after component removal.
  • Creates unnecessary work.

✅ Recommended

useEffect(() => {
  let active = true;

  async function load() {
    const data = await fetchData();

    if (active) {
      setData(data);
    }
  }

  load();

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

Why is this recommended?

  • Ignores completed requests after unmount.
  • Prevents invalid state updates.

7. Large Closures

Problem

A callback captures a huge object.

Not Recommended

const hugeArray = new Array(1000000);

function handleClick() {
  console.log(hugeArray.length);
}

Why is this bad?

  • Callback keeps a reference to a large object.
  • Garbage collector cannot free it while the callback is still referenced.

✅ Recommended

// Keep only the data you need.

const count = hugeArray.length;

function handleClick() {
  console.log(count);
}

// Also remove callbacks/listeners
// when they are no longer required.

Why is this recommended?

  • Reduces retained memory.
  • Allows large objects to be garbage collected sooner.

Best Practices Checklist

✔ Do❌ Don’t
Return a cleanup function from useEffectForget cleanup
Clear timersLeave intervals running
Remove event listenersKeep adding listeners
Abort fetch requestsIgnore pending requests
Close WebSocketsLeave connections open
Unsubscribe from subscriptionsKeep subscriptions alive
Disconnect observersLeave observers active

Always remember

Performance Problems

Performance problems make your application slower, but they do not necessarily increase memory usage.

  • Unnecessary Re-renders
    • Components render more often than needed.
    • Example: Parent component re-renders causing all child components to re-render.
  • Expensive Calculations
    • Heavy computations execute on every render.
    • Example: Sorting or filtering a large dataset repeatedly.
  • Large DOM Updates
    • React updates many DOM elements unnecessarily.
    • Example: Re-rendering an entire table when only one row changes.
  • Poor Memoization
    • Functions or computed values are recreated on every render.
    • Example: Not using useMemo, useCallback, or React.memo where appropriate.

Memory Problems (Memory Leaks)

Memory problems occur when objects remain in memory even after they are no longer needed, causing memory usage to grow over time.

  • Uncleared Timers
    • setInterval() or setTimeout() continues running after the component unmounts.
    • Solution: Use clearInterval() or clearTimeout() in the cleanup function.
  • Unremoved Event Listeners
    • Event listeners remain attached to the window or DOM elements.
    • Solution: Call removeEventListener() during cleanup.
  • Open WebSockets
    • WebSocket connections stay open after leaving the page.
    • Solution: Close the connection using socket.close().
  • Active Subscriptions
    • Observables or subscriptions continue receiving data after component unmount.
    • Solution: Call unsubscribe() in the cleanup function.
  • Uncancelled Async Operations
    • API requests complete after the component has been removed.
    • Solution: Cancel requests using AbortController or ignore the response.
  • Retained References
    • Closures, callbacks, or global variables continue referencing objects that are no longer needed.
    • Solution: Remove unnecessary references so the JavaScript Garbage Collector can reclaim memory.
Share Button

Leave a Reply

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