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.
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 :)
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;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With