Listening to window events without cleaning them up

Loading

Handling Window Events Properly in React

A common source of memory leaks in React applications comes from adding window event listeners without properly removing them when components unmount. Here’s how to handle window events correctly:

The Problem: Orphaned Event Listeners

function MyComponent() {
  const handleScroll = () => {
    console.log(window.scrollY);
  };

  useEffect(() => {
    // ❌ Missing cleanup - listener remains after component unmounts
    window.addEventListener('scroll', handleScroll);
  }, []);

  return <div>Scroll position tracker</div>;
}

Why This is Dangerous

  1. Memory Leaks: The component stays in memory because the window maintains a reference
  2. Performance Issues: Multiple listeners accumulate if component remounts
  3. Zombie Functions: The handler might try to update unmounted component state
  4. Stale Closures: Handlers may reference outdated state/props values

Correct Solutions

1. Basic Cleanup in useEffect

useEffect(() => {
  const handleScroll = () => {
    console.log(window.scrollY);
  };

  window.addEventListener('scroll', handleScroll);

  // ✅ Proper cleanup
  return () => {
    window.removeEventListener('scroll', handleScroll);
  };
}, []);

2. With Dependencies

function MyComponent({ threshold }) {
  useEffect(() => {
    const handleScroll = () => {
      if (window.scrollY > threshold) {
        console.log('Threshold passed');
      }
    };

    window.addEventListener('scroll', handleScroll);

    return () => {
      window.removeEventListener('scroll', handleScroll);
    };
  }, [threshold]); // Re-subscribes when threshold changes
}

3. Debounced Events

useEffect(() => {
  const handleResize = debounce(() => {
    console.log(window.innerWidth);
  }, 200);

  window.addEventListener('resize', handleResize);

  return () => {
    handleResize.cancel(); // Cancel any pending executions
    window.removeEventListener('resize', handleResize);
  };
}, []);

Class Component Implementation

class WindowSizeTracker extends React.Component {
  state = { width: window.innerWidth };

  handleResize = () => {
    this.setState({ width: window.innerWidth });
  };

  componentDidMount() {
    window.addEventListener('resize', this.handleResize);
  }

  componentWillUnmount() {
    window.removeEventListener('resize', this.handleResize);
  }

  render() {
    return <div>Window width: {this.state.width}px</div>;
  }
}

Common Window Events to Clean Up

  1. Scroll events (scroll)
  2. Resize events (resize)
  3. Keyboard events (keydown, keyup)
  4. Mouse events (mousemove, mouseup)
  5. Storage events (storage)
  6. Beforeunload events (beforeunload)

Advanced Patterns

Custom Hook for Window Events

function useWindowEvent(event, callback, dependencies = []) {
  useEffect(() => {
    window.addEventListener(event, callback);
    return () => window.removeEventListener(event, callback);
  }, [event, callback, ...dependencies]);
}

// Usage
function MyComponent() {
  useWindowEvent('mousemove', (e) => {
    console.log(e.clientX, e.clientY);
  });

  return <div>Mouse tracker</div>;
}

Throttled Event Listener

useEffect(() => {
  const handleScroll = throttle(() => {
    console.log('Scrolling...');
  }, 100);

  window.addEventListener('scroll', handleScroll);

  return () => {
    handleScroll.cancel();
    window.removeEventListener('scroll', handleScroll);
  };
}, []);

Best Practices

  1. Always remove event listeners in the cleanup function
  2. Use stable references for event handlers (useCallback if needed)
  3. Include all dependencies that the handler uses
  4. Consider debouncing/throttling for frequent events
  5. Test for memory leaks using browser dev tools

Debugging Tips

If you suspect listener leaks:

  1. Check getEventListeners(window) in Chrome DevTools
  2. Monitor memory usage when mounting/unmounting components
  3. Look for duplicate event handling
  4. Verify cleanup functions are being called

Remember: Any event listener added to global objects like window or document must be cleaned up when your component unmounts to prevent memory leaks and unexpected behavior.

Leave a Reply

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