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.
Related Articles
For related React debugging, check out how to debug React hooks and how to fix stale closures in React hooks.



