Search code examples
javascriptreactjsreact-hooksaddeventlistener

Click event added by addEventListener in useEffect get fired on mounting the component


I have two components, the parent (App) shows a button which on being clicked conditionally renders the Child component.

Here is the sandbox.

App.js

import { useState } from "react";
import Child from "./Child";

function App() {
  const [value, setValue] = useState(false);

  return (
    <div>
      <button onClick={() => setValue(true)}>Mount Child</button>
      {value ? <Child /> : null}
    </div>
  );
}

export default App;

Child.js

import React, { useEffect } from "react";

function Child() {
  const handleClick = () => {
    console.log("hi");
  };

  useEffect(() => {
    document.addEventListener("click", handleClick);

    return () => {
      document.removeEventListener("click", handleClick);
      console.log("unmounting");
    };
  });

  return <div>Child</div>;
}

export default Child;

Why does the event added here document.addEventListener("click", handleClick) get fired on mounting the Child?'.

This is the console after clicking the button:

unmounting 
hi 

Running in React.StrictMode component that unmounting is understandable, but I don't know why that "hi" gets logged.


Solution

  • This is odd behavior, but it seems the "Mount Child" button click is propagated to the document and the Child component's useEffect hook's callback adding the "click" event listener is still able to pick this click event up and trigger the handleClick callback.

    I suggest preventing the button's click event propagation... the user is clicking the button, not just anywhere in the document, right.

    Example:

    import { useState } from "react";
    import Child from "./Child";
    
    function App() {
      const [value, setValue] = useState(false);
    
      const clickHandler = e => {
        e.stopPropagation(); // <-- prevent click event propagation up the DOM
        setValue(true);
      }
    
      return (
        <div>
          <button onClick={clickHandler}>Mount Child</button>
          {value && <Child />}
        </div>
      );
    }
    
    export default App;
    

    Additionally, you've some logical errors in the Child component regarding the useEffect hook and event listener logic. The useEffect hook is missing the dependency array, so any time Child renders for any reason, it will remove and re-add the click event listener. handleClick is also declared outside the useEffect hook, so it's an external dependency that gets redeclared each render cycle and will also trigger the useEffect hook each render. It should be moved into the effect callback.

    Here we add an empty dependency array so the effect runs exactly once per component mounting, establishes the event listeners, and removes them when the component unmounts.

    Example:

    import React, { useEffect } from "react";
    
    function Child() {
      useEffect(() => {
        const handleClick = () => {
          console.log("hi");
        };
    
        document.addEventListener("click", handleClick);
    
        return () => {
          document.removeEventListener("click", handleClick);
          console.log("unmounting");
        };
      }, []);
    
      return <div>Child</div>;
    }
    
    export default Child;
    

    Edit click-event-added-by-addeventlistener-in-useeffect-get-fired-on-mounting-the-com