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
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:
I know that doesn’t fully answer your question, but that’s what I’ve got at this point 🙂
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
and it didn’t give me the hi at the start
This is odd behavior, but it seems the "Mount Child" button click is propagated to the
document
and theChild
component’suseEffect
hook’s callback adding the "click" event listener is still able to pick this click event up and trigger thehandleClick
callback.I suggest preventing the button’s click event propagation… the user is clicking the button, not just anywhere in the document, right.
Example:
Additionally, you’ve some logical errors in the
Child
component regarding theuseEffect
hook and event listener logic. TheuseEffect
hook is missing the dependency array, so any timeChild
renders for any reason, it will remove and re-add the click event listener.handleClick
is also declared outside theuseEffect
hook, so it’s an external dependency that gets redeclared each render cycle and will also trigger theuseEffect
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: