Forgetting to remove event listeners on unmount

Loading

Properly Managing Event Listeners in React Components

A critical but often overlooked aspect of React development is cleaning up event listeners when components unmount. Failing to do so can lead to memory leaks and unexpected behavior.

The Problem: Orphaned Event Listeners

import { useEffect } from 'react';

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

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

  return <div>My Component</div>;
}

Why This is Dangerous

  1. Memory Leaks: Event handlers maintain references to components
  2. Zombie Handlers: Attempts to update unmounted component state
  3. Performance Issues: Multiple redundant listeners accumulate
  4. Stale Closures: Handlers may reference outdated state/props

Correct Solutions

1. Basic Cleanup in useEffect

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

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

2. For Custom Events

useEffect(() => {
  const eventSource = new EventSource('/stream');
  eventSource.addEventListener('message', handleMessage);

  return () => {
    eventSource.removeEventListener('message', handleMessage);
    eventSource.close();
  };
}, []);

3. With Dependencies

useEffect(() => {
  const handler = () => {
    console.log('Current value:', someProp);
  };

  document.addEventListener('click', handler);

  return () => {
    document.removeEventListener('click', handler);
  };
}, [someProp]); // Re-subscribes when someProp changes

Class Component Example

class MyComponent extends React.Component {
  handleResize = () => {
    console.log('Window resized');
  };

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

  componentWillUnmount() {
    // ✅ Cleanup in class component
    window.removeEventListener('resize', this.handleResize);
  }

  render() {
    return <div>My Component</div>;
  }
}

Common Pitfalls

  1. Anonymous Functions:
   useEffect(() => {
     const handler = () => console.log('click');
     document.addEventListener('click', handler);

     return () => {
       // ❌ Won't work - different function instance
       document.removeEventListener('click', () => console.log('click'));
     };
   }, []);
  1. Missing Dependencies:
   useEffect(() => {
     const handler = () => console.log(someState);
     window.addEventListener('resize', handler);

     return () => window.removeEventListener('resize', handler);
     // ❌ Missing someState dependency
   }, []);
  1. Multiple Listeners:
   useEffect(() => {
     // ❌ Adds new listener on every render
     window.addEventListener('resize', handleResize);

     return () => {
       window.removeEventListener('resize', handleResize);
     };
   }); // ❌ Missing dependency array

Best Practices

  1. Always return a cleanup function from useEffect
  2. Use named handlers for consistent reference
  3. Include all dependencies in the dependency array
  4. Consider custom hooks for reusable listener logic
  5. Test for memory leaks using dev tools

Advanced Patterns

Custom Hook Example

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

// Usage
function MyComponent() {
  useWindowEvent('resize', () => {
    console.log('Window resized');
  });

  return <div>My Component</div>;
}

AbortController for Fetch Requests

useEffect(() => {
  const controller = new AbortController();

  fetch('/api', { signal: controller.signal })
    .then(response => response.json())
    .then(data => setData(data));

  return () => {
    controller.abort(); // Cancels the fetch on unmount
  };
}, []);

Remember: Proper cleanup is essential for maintaining application performance and preventing subtle bugs. Always consider what needs to be cleaned up when your component unmounts.

Leave a Reply

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