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.
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;