logoESLint React
Rules

no-leaked-event-listener

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

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

Presets

web-api recommended recommended-typescript recommended-type-checked strict strict-typescript strict-type-checked

Rule Details

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

Common Violations

Invalid

import React, { Component } from "react";

class MyComponent extends Component {
  componentDidMount() {
    document.addEventListener("click", this.handleClick);
    //                                 ^^^ An 'addEventListener' in 'componentDidMount' should have a corresponding 'removeEventListener' in the '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);
    //                                 ^^^ An '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"));
    //                                 ^^^ An '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 });
    //                                 ^^^ An '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);
    //                                 ^^^ An 'addEventListener' in 'useEffect' should have a corresponding 'removeEventListener' in its cleanup function.
  }, []);
}
import { useEffect } from "react";

function MyComponent() {
  useEffect(() => {
    if (!el) {
      return;
    }
    // The following cases are intentionally considered possible leaks:
    for (const [name, handler] of Object.entries(handlers)) {
      //                          ^^^ The entries are not guaranteed to be the same as those in the effect cleanup function.
      el.addEventListener(name, handler);
    }

    return () => {
      for (const [name, handler] of Object.entries(handlers)) {
        //                          ^^^ The entries are not guaranteed to be the same as those in the effect setup function.
        el.removeEventListener(name, handler);
      }
    };
  }, [el]);

  return null;
}

Valid

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);
    };
  }, []);
}
import { useEffect } from "react";

function MyComponent() {
  useEffect(() => {
    if (!el) {
      return;
    }

    // Instead, always use the same entries in both the setup and cleanup functions:
    const handlerEntries = Object.entries(handlers); // <- Use the same entries array

    for (const [name, handler] of handlerEntries) {
      el.addEventListener(name, handler);
    }

    return () => {
      for (const [name, handler] of handlerEntries) {
        el.removeEventListener(name, handler);
      }
    };
  }, [el]);
  return null;
}

Resources

Further Reading


See Also

On this page