Next.js starter your AI actually understands. Ship internal tools in days not weeks. Pre-order $199 $499 → [Get it now]

How to fix memory leaks in React

Memory leaks in React occur when components don’t properly clean up subscriptions, timers, or event listeners, causing memory usage to grow over time. As the creator of CoreUI with 12 years of React development experience, I’ve debugged memory leaks in production applications that caused browser crashes after extended use, and learned that proper cleanup in useEffect is essential for long-running applications.

The most reliable solution uses cleanup functions in useEffect to cancel subscriptions and remove listeners.

Fix Event Listener Leaks

import { useEffect } from 'react'

// BAD - Memory leak
function BadComponent() {
  useEffect(() => {
    const handleResize = () => console.log(window.innerWidth)
    window.addEventListener('resize', handleResize)
    // Missing cleanup!
  }, [])

  return <div>Content</div>
}

// GOOD - Proper cleanup
function GoodComponent() {
  useEffect(() => {
    const handleResize = () => console.log(window.innerWidth)
    window.addEventListener('resize', handleResize)

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

  return <div>Content</div>
}

Fix Timer Leaks

import { useEffect, useState } from 'react'

// BAD - Memory leak
function BadTimer() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    setInterval(() => {
      setCount(c => c + 1)
    }, 1000)
    // Timer continues after unmount!
  }, [])

  return <div>{count}</div>
}

// GOOD - Proper cleanup
function GoodTimer() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    const timer = setInterval(() => {
      setCount(c => c + 1)
    }, 1000)

    return () => {
      clearInterval(timer)
    }
  }, [])

  return <div>{count}</div>
}

// setTimeout also needs cleanup
function DelayedComponent() {
  const [show, setShow] = useState(false)

  useEffect(() => {
    const timeout = setTimeout(() => {
      setShow(true)
    }, 3000)

    return () => {
      clearTimeout(timeout)
    }
  }, [])

  return <div>{show ? 'Visible' : 'Hidden'}</div>
}

Fix Fetch Request Leaks

import { useEffect, useState } from 'react'

// BAD - setState after unmount causes warning
function BadFetch({ userId }) {
  const [user, setUser] = useState(null)

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => setUser(data))
      // If component unmounts, setState still called!
  }, [userId])

  return <div>{user?.name}</div>
}

// GOOD - Cancel fetch on unmount
function GoodFetch({ userId }) {
  const [user, setUser] = useState(null)

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

    fetch(`/api/users/${userId}`, {
      signal: controller.signal
    })
      .then(res => res.json())
      .then(data => setUser(data))
      .catch(err => {
        if (err.name !== 'AbortError') {
          console.error(err)
        }
      })

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

  return <div>{user?.name}</div>
}

// Alternative with flag
function FetchWithFlag({ userId }) {
  const [user, setUser] = useState(null)

  useEffect(() => {
    let cancelled = false

    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        if (!cancelled) {
          setUser(data)
        }
      })

    return () => {
      cancelled = true
    }
  }, [userId])

  return <div>{user?.name}</div>
}

Fix Subscription Leaks

import { useEffect, useState } from 'react'

// WebSocket subscription
function ChatComponent() {
  const [messages, setMessages] = useState([])

  useEffect(() => {
    const ws = new WebSocket('ws://localhost:3000')

    ws.onmessage = (event) => {
      const message = JSON.parse(event.data)
      setMessages(prev => [...prev, message])
    }

    ws.onerror = (error) => {
      console.error('WebSocket error:', error)
    }

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

  return (
    <ul>
      {messages.map(msg => (
        <li key={msg.id}>{msg.text}</li>
      ))}
    </ul>
  )
}

// Event emitter subscription
function EventComponent() {
  const [data, setData] = useState(null)

  useEffect(() => {
    const handleData = (newData) => {
      setData(newData)
    }

    eventEmitter.on('data', handleData)

    return () => {
      eventEmitter.off('data', handleData)
    }
  }, [])

  return <div>{data}</div>
}

Fix Observable Leaks

import { useEffect, useState } from 'react'
import { fromEvent } from 'rxjs'
import { debounceTime } from 'rxjs/operators'

function SearchComponent() {
  const [query, setQuery] = useState('')

  useEffect(() => {
    const input = document.querySelector('#search')

    const subscription = fromEvent(input, 'input')
      .pipe(debounceTime(300))
      .subscribe((event) => {
        setQuery(event.target.value)
      })

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

  return (
    <div>
      <input id="search" type="text" />
      <p>Query: {query}</p>
    </div>
  )
}

Fix Closure Memory Leaks

import { useEffect, useRef, useState } from 'react'

// BAD - Closure captures large data
function BadClosureComponent() {
  const [count, setCount] = useState(0)
  const largeData = new Array(1000000).fill('data')

  useEffect(() => {
    const interval = setInterval(() => {
      // This closure captures largeData!
      console.log(count, largeData.length)
    }, 1000)

    return () => clearInterval(interval)
  }, [count]) // Re-creates interval with new closure

  return <div>{count}</div>
}

// GOOD - Use ref to avoid capturing
function GoodClosureComponent() {
  const [count, setCount] = useState(0)
  const largeData = useRef(new Array(1000000).fill('data'))

  useEffect(() => {
    const interval = setInterval(() => {
      console.log(count, largeData.current.length)
    }, 1000)

    return () => clearInterval(interval)
  }, [count])

  return <div>{count}</div>
}

Custom Hook for Safe Async

import { useEffect, useRef } from 'react'

function useSafeAsync() {
  const isMounted = useRef(true)

  useEffect(() => {
    return () => {
      isMounted.current = false
    }
  }, [])

  const safeSetState = (callback) => {
    if (isMounted.current) {
      callback()
    }
  }

  return safeSetState
}

// Usage
function UserProfile({ userId }) {
  const [user, setUser] = useState(null)
  const safeSetState = useSafeAsync()

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        safeSetState(() => setUser(data))
      })
  }, [userId, safeSetState])

  return <div>{user?.name}</div>
}

Detect Memory Leaks

import { useEffect, useRef } from 'react'

function useMemoryLeakDetector(componentName) {
  const renderCount = useRef(0)

  renderCount.current++

  useEffect(() => {
    console.log(`[${componentName}] Mounted (render #${renderCount.current})`)

    return () => {
      console.log(`[${componentName}] Unmounted (render #${renderCount.current})`)
    }
  }, [componentName])
}

function MyComponent() {
  useMemoryLeakDetector('MyComponent')

  // If you see "Mounted" without matching "Unmounted",
  // there might be a memory leak preventing cleanup

  return <div>Content</div>
}

Best Practice Note

This is how we prevent memory leaks in all CoreUI React components. Always return cleanup functions from useEffect to cancel subscriptions, clear timers, remove event listeners, and abort fetch requests. Use AbortController for fetch cancellation, refs to avoid capturing large objects in closures, and custom hooks to ensure safe async operations. Test component unmounting in development to verify cleanup functions execute properly.

For production applications, consider using CoreUI’s React Admin Template which follows these patterns throughout the codebase.

For related React debugging, check out how to debug React hooks and how to fix stale closures in React hooks.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.

Answers by CoreUI Core Team