I create an app using React hooks. I have a helper function onClickOutsideHook(ref, callback)
that trigger the callback
when you click outside of the component that provide the ref
using React.useRef
:
export const onClickOutsideHook = (ref, callback) => {
// Hook get from https://stackoverflow.com/a/42234988/8583669
React.useEffect(() => {
const handleClickOutside = event => {
if (ref?.current && !ref.current.contains(event.target)) {
callback();
}
};
// Bind the event listener
document.addEventListener("mousedown", handleClickOutside);
return () => {
// Unbind the event listener on clean up
document.removeEventListener("mousedown", handleClickOutside);
};
}, [callback, ref]);
};
I have a component Dropdown
that use this helper so it close when you click outside of it. This component has a Modal
component as children that use ReactDOM.createPortal
. I use it to render the Modal
in body
so it can cover all the app screen. My Modal
contain a button that alert a message when you clicked on it:
function Modal() {
return ReactDOM.createPortal(
<div
style={{
position: "absolute",
top: 0,
left: 0,
height: "100%",
width: "100%",
background: "rgba(0,0,0,0.6)"
}}
>
<button onClick={() => alert("Clicked on Modal")}>Click</button>
</div>,
document.body
);
}
function Dropdown(props) {
const [isModalOpen, setIsModalOpen] = React.useState(false);
const dropdownRef = React.useRef(null);
onClickOutsideHook(dropdownRef, props.onClose);
return (
<div ref={dropdownRef}>
Click outside and I will close
<button onClick={() => setIsModalOpen(true)}>open Modal</button>
{isModalOpen ? <Modal /> : <></>}
</div>
);
}
The problem is when I click on the Modal
button to trigger the alert, the Dropdown
is closed before since I clicked outside of it (Modal
is not rendered as a children of Dropdown
but in body
). So my alert is never triggered.
Is there a way to define Modal
as a children of Dropdown
using ref
but still render it in body
using ReactDOM.createPortal
?
Just have a look to the CodeSandbox.
Portals provide a first-class way to render children into a DOM node that exists outside the DOM hierarchy of the parent component. ReactDOM. createPortal(child, container) The first argument ( child ) is any renderable React child, such as an element, string, or fragment.
render() The first argument is the element or component we want to render, and the second argument is the HTML element (the target node) to which we want to append it. Generally, when we create our project with create-react-app , it gives us a div with the id of a root inside index.
Render a React element into the DOM in the supplied container and return a reference to the component (or returns null for stateless components). If the React element was previously rendered into container , this will perform an update on it and only mutate the DOM as necessary to reflect the latest React element.
Creating Refs Refs are created using React. createRef() and attached to React elements via the ref attribute. Refs are commonly assigned to an instance property when a component is constructed so they can be referenced throughout the component.
Like the Portals docs says:
Even though a portal can be anywhere in the DOM tree, it behaves like a normal React child in every other way ...
This includes event bubbling. An event fired from inside a portal will propagate to ancestors in the containing React tree, even if those elements are not ancestors in the DOM tree.
But here is not the case, the mousedown event listener was added on the document and not only the Dropdown component. Even so, the bubbling still happens.
This means that if you add a ref to the Modal, and then add an event listener on the mousedown event with the whole purpose to stop the propagation, the handleClickOutside function will never be called.
It may still seem like a workaround, I don't know if there is a proper way to check.
function Modal() {
const modalRef = useRef();
useEffect(() => {
const stopPropagation = e => {
e.stopPropagation();
};
const { current: modalDom } = modalRef;
modalDom.addEventListener("mousedown", stopPropagation);
return () => {
modalDom.removeEventListener("mousedown", stopPropagation);
};
}, []);
return ReactDOM.createPortal(
<div
ref={modalRef}
style={{
position: "absolute",
top: 0,
left: 0,
height: "100%",
width: "100%",
background: "rgba(0,0,0,0.6)"
}}
>
<button onClick={() => alert("Clicked on Modal")}>Click</button>
</div>,
document.body
);
}
Watch the Modal component from the following CodeSandbox.
As a workaround you can add an ID attribute to your modal and then check if the click was outside the modal
function Modal() {
return ReactDOM.createPortal(
<div id="modalId">
<button onClick={() => alert("Clicked on Modal")}>Click</button>
</div>,
document.body
);
}
...
React.useEffect(() => {
const handleClickOutside = (event) => {
if (
ref?.current &&
!ref.current.contains(event.target) &&
document.getElementById("modalId") &&
!document.getElementById("modalId").contains(event.target) // check if click was outside your modal
) {
callback();
}
};
// Bind the event listener
document.addEventListener("mousedown", handleClickOutside);
return () => {
// Unbind the event listener on clean up
document.removeEventListener("mousedown", handleClickOutside);
};
}, [callback, ref]);
...
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