Documentation
Rules
web-api/no-leaked-event-listener

no-leaked-event-listener

Rule category

Correctness.

What it does

Enforce that every addEventListener in a component or custom hook has a corresponding removeEventListener.

Why is this bad?

Adding an event listener without removing it can lead to memory leaks and unexpected behavior. This is because the event listener will continue to exist even after the component or hook is unmounted.

Examples

Failing

import React, { Component } from 'react';
 
class Example extends Component {
  componentDidMount() {
    document.addEventListener('click', this.handleClick);
    //                                 ^^^^^^^^^^^^^^^^
    //                                 - A 'addEventListener' in 'componentDidMount' should have a corresponding 'removeEventListener' in 'componentWillUnmount' method.
  }
 
  handleClick() {
    console.log('clicked');
  }
 
  render() {
    return null;
  }
}
import React, { useEffect } from 'react';
 
function Example() {
  useEffect(() => {
    const handleClick = () => {
      console.log('clicked');
    };
 
    document.addEventListener('click', handleClick);
    //                                 ^^^^^^^^^^^
    //                                 - A 'addEventListener' in 'useEffect' should have a corresponding 'removeEventListener' in its cleanup function.
  }, []);
 
  return null;
}
import React, { useEffect } from 'react';
 
function Example() {
  useEffect(() => {
    document.addEventListener('click', () => console.log('clicked'));
    //                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    //                                 - A 'addEventListener' should not have an inline listener function.
  }, []);
 
  return null;
}
import React, { useEffect } from 'react';
 
function Example() {
  useEffect(() => {
    const handleClick = () => {
      console.log('clicked');
    };
 
    document.addEventListener('click', handleClick, { capture: true });
    //                                 ^^^^^^^^^^^
    //                                 - A 'addEventListener' in 'useEffect' should have a corresponding 'removeEventListener' in its cleanup function.
 
    return () => {
      document.removeEventListener('click', handleClick, { capture: false });
    };
  }, []);
 
  return null;
}
function useCustomHook() {
  useEffect(() => {
    const handleClick = () => {
      console.log('clicked');
    };
 
    document.addEventListener('click', handleClick);
    //                                 ^^^^^^^^^^^^
    //                                 - A 'addEventListener' in 'useEffect' should have a corresponding 'removeEventListener' in its cleanup function.
  }, []);
}

Passing

import React, { Component } from 'react';
 
class Example extends Component {
  componentDidMount() {
    document.addEventListener('click', this.handleClick);
  }
 
  componentWillUnmount() {
    document.removeEventListener('click', this.handleClick);
  }
 
  handleClick() {
    console.log('clicked');
  }
 
  render() {
    return null;
  }
}
import React, { useEffect } from 'react';
 
function Example() {
  useEffect(() => {
    const handleClick = () => {
      console.log('clicked');
    };
 
    document.addEventListener('click', handleClick);
 
    return () => {
      document.removeEventListener('click', handleClick);
    };
  }, []);
 
  return null;
}
import React, { useEffect } from 'react';
 
function Example() {
  useEffect(() => {
    const handleClick = () => {
      console.log('clicked');
    };
 
    document.addEventListener('click', handleClick, { capture: true });
 
    return () => {
      document.removeEventListener('click', handleClick, { capture: true });
    };
  }, []);
 
  return null;
}
function useCustomHook() {
  useEffect(() => {
    const handleClick = () => {
      console.log('clicked');
    };
 
    document.addEventListener('click', handleClick);
 
    return () => {
      document.removeEventListener('click', handleClick);
    };
  }, []);
}

Further Reading