Forgetting to remove event listeners on unmount

Loading

A common memory leak issue in React occurs when event listeners aren’t properly removed when components unmount. Here’s how to handle this correctly:

The Problem

When you add event listeners but don’t remove them:

function ProblemComponent() {
  useEffect(() => {
    window.addEventListener('resize', handleResize);
    // ❌ Missing cleanup - listener stays after unmount
  }, []);

  const handleResize = () => {
    console.log('Window resized');
  };

  return <div>Resize the window</div>;
}

Consequences:

  • Memory leaks (listeners keep referencing unmounted components)
  • Potential errors when handlers try to update unmounted component state
  • Performance degradation over time

The Solution: Cleanup Function

Class Components

class SafeComponent extends React.Component {
  componentDidMount() {
    window.addEventListener('resize', this.handleResize);
  }

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

  handleResize = () => {
    console.log('Window resized');
  };

  render() {
    return <div>Resize the window</div>;
  }
}

Functional Components (with Hooks)

function SafeComponent() {
  const handleResize = () => {
    console.log('Window resized');
  };

  useEffect(() => {
    window.addEventListener('resize', handleResize);

    // Cleanup function
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  return <div>Resize the window</div>;
}

Common Scenarios Requiring Cleanup

1. Custom Event Listeners

useEffect(() => {
  const handleKeyDown = (e) => {
    if (e.key === 'Escape') closeModal();
  };

  document.addEventListener('keydown', handleKeyDown);

  return () => {
    document.removeEventListener('keydown', handleKeyDown);
  };
}, [closeModal]);

2. setTimeout/setInterval

useEffect(() => {
  const timer = setTimeout(() => {
    console.log('Delayed action');
  }, 1000);

  return () => clearTimeout(timer);
}, []);

3. WebSocket/API Subscriptions

useEffect(() => {
  const socket = new WebSocket('ws://example.com');

  socket.onmessage = (event) => {
    console.log('Message:', event.data);
  };

  return () => {
    socket.close();
  };
}, []);

4. Third-party Library Initialization

useEffect(() => {
  const map = new MapLibrary('#map-container', options);

  return () => {
    map.destroy();
  };
}, [options]);

Best Practices

  1. Always return a cleanup function from useEffect
  2. Use the same reference for add/remove
  3. Handle dependencies properly:
   useEffect(() => {
     // Effect code

     return () => {
       // Cleanup code
     };
   }, [dependencies]); // Proper dependency array
  1. For anonymous functions, store in a variable:
   useEffect(() => {
     const handler = () => console.log('Event');
     element.addEventListener('event', handler);

     return () => element.removeEventListener('event', handler);
   }, []);
  1. Use custom hooks for reusable logic:
   function useEventListener(eventName, handler, element = window) {
     useEffect(() => {
       element.addEventListener(eventName, handler);

       return () => {
         element.removeEventListener(eventName, handler);
       };
     }, [eventName, handler, element]);
   }

Debugging Tips

  1. Check listener count in dev tools (Chrome’s Event Listeners panel)
  2. Add console logs to cleanup functions
  3. Use React StrictMode to detect missing cleanups
  4. Monitor memory usage for leaks

Complete Example

function ScrollLogger() {
  const [scrollPosition, setScrollPosition] = useState(0);

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

    window.addEventListener('scroll', handleScroll);

    return () => {
      window.removeEventListener('scroll', handleScroll);
      console.log('Scroll listener cleaned up');
    };
  }, []);

  return (
    <div>
      <p>Current scroll position: {scrollPosition}px</p>
      <div style={{ height: '200vh' }}>Scroll down</div>
    </div>
  );
}

Leave a Reply

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