Listening to window events without cleaning them up

Loading

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:

  1. The listener remains active even after component unmounts
  2. The component instance can’t be garbage collected
  3. Multiple mounts create duplicate listeners
  4. 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

  1. Scroll events
   window.addEventListener('scroll', handler);
  1. Resize events
   window.addEventListener('resize', handler);
  1. Keyboard events
   window.addEventListener('keydown', handler);
  1. Mouse events
   window.addEventListener('mousemove', handler);
  1. 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

  1. Check listener count in Chrome DevTools:
  • Open DevTools → Elements panel
  • Select “window” in the DOM tree
  • View “Event Listeners” tab
  1. Use React DevTools to verify components unmount
  2. 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

  1. Always remove window event listeners in cleanup functions
  2. Use the same function reference for add/remove
  3. For anonymous functions, store them in variables
  4. Consider custom hooks for reusable event logic
  5. Debounce intensive events but remember to cancel them
  6. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *