shahriyar.dev
Back to blog
reactuseEffectcleanupmemory-leaksside-effectshooks

The Hidden Danger in Your React Components: Why Every useEffect Needs a Cleanup Function

·3 min read

There are many scenarios where we need to return a cleanup function from useEffect. This pattern is essential for preventing memory leaks and ensuring your React components behave predictably over time.

The key rule is simple: any time you start a side effect inside useEffect that continues running after the component renders, you must return a cleanup function that will be called when the component unmounts.

Think about it — if you've ever started a counter using setInterval or opened a WebSocket connection that continuously receives messages, you’ll want to stop listening or cancel that interval when the component leaves the DOM. Otherwise, your app will keep running background tasks for components that no longer exist, wasting resources and potentially causing errors.

Let's see this in action

js
import React, { useState, useEffect } from 'react'

const Counter = () => {
  const [count, setCount] = useState(0)

  useEffect(() => {
    const interval = setInterval(() => {
      setCount(prev => prev + 1)
      console.log("Counting")
    }, 1000)
  })

  return (
    <div>Counter {count}</div>
  )
}

export default Counter

At first glance, this looks fine. When the component renders, it starts a timer that increments the count every second and logs "Counting" to the console. The user sees the counter ticking upward in the DOM.

But here's the problem: if you conditionally remove this component from the DOM — say by toggling it off with a parent state — the interval keeps running. The console still prints "Counting" every second, even though the component is gone. That’s a memory leak and wasted computation.

The fix: return a cleanup function

To prevent this, we simply return a function from useEffect that stops the interval:

js
useEffect(() => {
    const interval = setInterval(() => {
      setCount(prev => prev + 1)
      console.log("Counting")
    }, 1000)

    return () => {
      clearInterval(interval)
    }
  })

Now when the component unmounts, React calls the cleanup function, which clears the interval. No more unnecessary logging, no more background tasks running for a dead component.

Why this matters

  • Memory leaks — Unmounted components can still hold references to timers, subscriptions, or event listeners, preventing garbage collection.
  • Performance — Unnecessary background work slows down your app and drains battery on mobile devices.
  • Correctness — Without cleanup, you might update state on an unmounted component, leading to React warnings and unpredictable UI.

Common use cases for cleanup

  • setInterval / setTimeout — Clear the timer
  • WebSocket connections — Close the connection
  • Event listeners — Remove with removeEventListener
  • API subscriptions — Unsubscribe when component leaves
  • ResizeObserver or IntersectionObserver — Disconnect the observer

Final thought

The rule is straightforward: if you set something up in useEffect, tear it down in the returned function. This discipline keeps your React components clean, efficient, and bug-free — especially in apps where components mount and unmount frequently.

Comments