A particularly insidious type of memory leak occurs when you add event listeners to the window
object but fail to remove them when your React component unmounts. Here’s what you need to know:
Why This Is Problematic
When you attach event listeners to window
without cleanup:
function LeakyComponent() {
useEffect(() => {
window.addEventListener('scroll', () => {
console.log('Scrolling!');
});
// ❌ No cleanup - this is bad!
}, []);
return <div>Scroll the page</div>;
}
What happens:
- The listener remains active even after component unmounts
- The component instance can’t be garbage collected
- Multiple mounts create duplicate listeners
- Event handlers might try to update unmounted component state
The Proper Solution
For Functional Components (Hooks)
function SafeComponent() {
useEffect(() => {
const handleScroll = () => {
console.log('Scrolling:', window.scrollY);
};
window.addEventListener('scroll', handleScroll);
// Cleanup function removes the listener
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []); // Empty dependency array = runs once on mount
return <div>Scroll safely</div>;
}
For Class Components
class SafeComponent extends React.Component {
handleScroll = () => {
console.log('Scrolling:', window.scrollY);
};
componentDidMount() {
window.addEventListener('scroll', this.handleScroll);
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll);
}
render() {
return <div>Scroll safely</div>;
}
}
Common Window Events That Need Cleanup
- Scroll events
window.addEventListener('scroll', handler);
- Resize events
window.addEventListener('resize', handler);
- Keyboard events
window.addEventListener('keydown', handler);
- Mouse events
window.addEventListener('mousemove', handler);
- Hash/popstate changes
window.addEventListener('popstate', handler);
Advanced Patterns
Custom Hook Solution
Create a reusable hook for window events:
function useWindowEvent(eventName, handler) {
useEffect(() => {
window.addEventListener(eventName, handler);
return () => window.removeEventListener(eventName, handler);
}, [eventName, handler]); // Re-run if handler or eventName changes
}
// Usage
function Component() {
useWindowEvent('resize', () => {
console.log('Window resized');
});
return <div>Resize me</div>;
}
Debounced Events
For performance-intensive events like scroll/resize:
import { debounce } from 'lodash';
function Component() {
useEffect(() => {
const debouncedHandler = debounce(() => {
console.log('Debounced scroll');
}, 100);
window.addEventListener('scroll', debouncedHandler);
return () => {
debouncedHandler.cancel(); // Important!
window.removeEventListener('scroll', debouncedHandler);
};
}, []);
return <div>Scroll with debounce</div>;
}
Debugging Memory Leaks
- Check listener count in Chrome DevTools:
- Open DevTools → Elements panel
- Select “window” in the DOM tree
- View “Event Listeners” tab
- Use React DevTools to verify components unmount
- Profile memory usage:
- Take heap snapshot before/after component mounts/unmounts
- Compare snapshots for retained memory
Real-World Example
Here’s a complete implementation of a scroll-to-top button that cleans up properly:
function ScrollToTopButton() {
const [visible, setVisible] = useState(false);
useEffect(() => {
const handleScroll = () => {
setVisible(window.scrollY > 300);
};
window.addEventListener('scroll', handleScroll);
// Set initial state
handleScroll();
return () => window.removeEventListener('scroll', handleScroll);
}, []);
const scrollToTop = () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
};
return visible ? (
<button
onClick={scrollToTop}
style={{
position: 'fixed',
bottom: '20px',
right: '20px'
}}
>
↑ Top
</button>
) : null;
}
Key Takeaways
- Always remove window event listeners in cleanup functions
- Use the same function reference for add/remove
- For anonymous functions, store them in variables
- Consider custom hooks for reusable event logic
- Debounce intensive events but remember to cancel them
- Verify cleanup using browser dev tools
Proper cleanup of window events ensures your React applications remain memory-efficient and bug-free as components mount and unmount.