DocumentationRulesno-leaked-event-listener

no-leaked-event-listener

Full Name in eslint-plugin-react-web-api

react-web-api/no-leaked-event-listener

Full Name in @eslint-react/eslint-plugin

@eslint-react/web-api/no-leaked-event-listener

Features

🔍

Presets

  • web-api
  • recommended
  • recommended-typescript
  • recommended-type-checked

What it does

Enforces that every addEventListener in a component or custom Hook has a corresponding removeEventListener.

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 MyComponent 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 MyComponent() {
  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 MyComponent() {
  useEffect(() => {
    document.addEventListener("click", () => console.log("clicked"));
    //                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    //                                 - A 'addEventListener' should not have an inline listener function.
  }, []);
 
  return null;
}
import React, { useEffect } from "react";
 
function MyComponent() {
  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 MyComponent 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 MyComponent() {
  useEffect(() => {
    const handleClick = () => {
      console.log("clicked");
    };
 
    document.addEventListener("click", handleClick);
 
    return () => {
      document.removeEventListener("click", handleClick);
    };
  }, []);
 
  return null;
}
import React, { useEffect } from "react";
 
function MyComponent() {
  useEffect(() => {
    const handleClick = () => {
      console.log("clicked");
    };
 
    document.addEventListener("click", handleClick, { capture: true });
 
    return () => {
      document.removeEventListener("click", handleClick, { capture: true });
    };
  }, []);
 
  return null;
}
import { useEffect } from "react";
 
function MyComponent() {
  useEffect(() => {
    const events = ["mousemove", "mousedown", "keydown", "scroll", "touchstart"];
    const handleActivity = () => {};
 
    events.forEach((event) => {
      window.addEventListener(event, handleActivity);
    });
 
    return () => {
      events.forEach((event) => {
        window.removeEventListener(event, handleActivity);
      });
    };
  }, []);
 
  return null;
}
import { useEffect } from "react";
 
function MyComponent() {
  useEffect(() => {
    const events = [
      ["mousemove", () => {}],
      ["mousedown", () => {}],
    ];
 
    for (const [event, handler] of events) {
      window.addEventListener(event, handler);
    }
 
    return () => {
      for (const [event, handler] of events) {
        window.removeEventListener(event, handler);
      }
    };
  }, []);
 
  return null;
}
function useCustomHook() {
  useEffect(() => {
    const handleClick = () => {
      console.log("clicked");
    };
 
    document.addEventListener("click", handleClick);
 
    return () => {
      document.removeEventListener("click", handleClick);
    };
  }, []);
}

Implementation

Further Reading