Forgetting to remove event listeners on unmount

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

  1. Forgetting the cleanup function entirely:
   useEffect(() => {
     window.addEventListener('resize', handleResize);
     // ❌ Missing cleanup
   }, []);
  1. Using anonymous functions:
   useEffect(() => {
     window.addEventListener('resize', () => {
       console.log('Resized');
     });
     // ❌ Can't remove anonymous function
     return () => window.removeEventListener('resize', ???);
   }, []);
  1. 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

  1. Always return a cleanup function from useEffect
  2. Store handler references to ensure proper removal
  3. Use named functions when possible
  4. Include all dependencies in the dependency array
  5. 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

Leave a Reply

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