Forgetting to Remove Event Listeners on Unmount in React
A common memory leak issue in React occurs when event listeners aren’t properly cleaned up when components unmount. This can lead to:
- Memory leaks
- Performance degradation
- “Zombie” event handlers executing on unmounted components
- Strange application behavior
The Problem
function ProblemComponent() {
useEffect(() => {
// ❌ Listener never gets removed
window.addEventListener('resize', handleResize);
}, []);
const handleResize = () => {
console.log('Window resized');
};
return <div>Content</div>;
}
Correct Solutions
1. Basic Cleanup (Recommended)
function FixedComponent() {
useEffect(() => {
const handleResize = () => {
console.log('Window resized');
};
window.addEventListener('resize', handleResize);
// ✅ Cleanup function
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
}
2. For Multiple Listeners
function MultiListenerComponent() {
useEffect(() => {
const handleClick = () => console.log('Clicked');
const handleKeyDown = () => console.log('Key pressed');
document.addEventListener('click', handleClick);
window.addEventListener('keydown', handleKeyDown);
// ✅ Cleanup all listeners
return () => {
document.removeEventListener('click', handleClick);
window.removeEventListener('keydown', handleKeyDown);
};
}, []);
}
3. With Event Parameters
function ParameterizedComponent() {
useEffect(() => {
const handleScroll = (e) => {
console.log('Scrolled:', e.target.scrollTop);
};
const element = document.getElementById('scrollable');
element.addEventListener('scroll', handleScroll);
return () => {
element.removeEventListener('scroll', handleScroll);
};
}, []);
}
Common Mistakes
- Forgetting the cleanup function entirely:
useEffect(() => {
window.addEventListener('resize', handleResize);
// ❌ Missing cleanup
}, []);
- Using anonymous functions:
useEffect(() => {
window.addEventListener('resize', () => {
console.log('Resized');
});
// ❌ Can't remove anonymous function
return () => window.removeEventListener('resize', ???);
}, []);
- Incorrect dependency arrays:
const [count, setCount] = useState(0);
useEffect(() => {
const handler = () => console.log(count);
window.addEventListener('click', handler);
return () => window.removeEventListener('click', handler);
}, []); // ❌ Missing count dependency
Best Practices
- Always return a cleanup function from useEffect
- Store handler references to ensure proper removal
- Use named functions when possible
- Include all dependencies in the dependency array
- Consider custom hooks for reusable logic
Advanced Patterns
1. Custom Hook Solution
function useEventListener(eventName, handler, element = window) {
useEffect(() => {
element.addEventListener(eventName, handler);
return () => element.removeEventListener(eventName, handler);
}, [eventName, handler, element]);
}
// Usage
function Component() {
useEventListener('click', () => console.log('Clicked'));
return <div>Content</div>;
}
2. For Class Components
class ResizeComponent extends React.Component {
handleResize = () => {
console.log('Resized');
};
componentDidMount() {
window.addEventListener('resize', this.handleResize);
}
componentWillUnmount() {
// ✅ Cleanup in unmount
window.removeEventListener('resize', this.handleResize);
}
render() {
return <div>Content</div>;
}
}
3. With AbortController
function FetchComponent() {
useEffect(() => {
const controller = new AbortController();
fetch('/api', { signal: controller.signal })
.then(response => response.json())
.then(data => console.log(data));
return () => controller.abort(); // Aborts fetch on unmount
}, []);
}
- Event listeners
- Subscriptions
- Timers/intervals
- Network requests
- WebSocket connections
