skip to Main Content

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.

3

Answers


  1. Ok. I still don’t understand why the click handler is called on mount. I noticed that if you changed the eventListener to something other than click, it does not fire on mount. That implies that the click handler is somehow being called from the click that mounts the child. Seems very out of order and I hope that someone can explain why that happens.

    In the meantime, here is a solution that will produce the desired result. Create a piece of state in the child component that is set to true after the component has mounted. Then, only call the handler if this piece of state is true. The child component would then look like this:

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

    I know that doesn’t fully answer your question, but that’s what I’ve got at this point 🙂

    Login or Signup to reply.
  2. it has to do something with the click event somehow running after the child component has rendered even though you clicked only once

    I tried debugging the issue by selecting the DOM element from developer tools and firing the click event without clicking

    enter image description here

    and it didn’t give me the hi at the start

    Login or Signup to reply.
  3. 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

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search