When it comes to handling modals, dialogues, tooltips or hover cards, your best friend is React Portal. In short, it does what it's named for, ports a component to the specified location. A location, or better to call a container, can be any element in the DOM, even outside the React root component. That's why it is magic.
The portal is defined in react-dom
library as a named export and has the following syntax:
import { createPortal } from 'react-dom';
createPortal(child, container);
A child
represents the content you wish to render. It can be any HTML element, string or React Fragment. A container
must be a target DOM element where the content will be rendered.
The most common use case for portals is modals. Normally React renders a returned element as a child to its closest parent component. This starts to become a problem when a parent component has styles such as a relative position, z-index or hidden overflow. This prevents child element to break out from the parent element boundaries. As the desired behaviour of modals or dialogs is to be rendered on top of other elements, portals provide an elegant way to render children outside the React tree and escape any style restrictions. The following example illustrates how the DOM elements are rendered when using a portal:
import React from "react";
import { createPortal } from 'react-dom';
export default function App() {
return (
<div className="parent">
{createPortal(<div className="child">Child content</div>, document.body)}
</div>
);
}
This will yield a little bit unussual DOM structure, where the child component is mounted outside the parent:
<body>
<div id="root">
<div class="parent"></div>
</div>
<div class="child">Child content</div>
</body>
But what is more fascinating, is if you would open React DevTools, you could see, that the rendered element is a direct child component of the App
parent component. It means, that the child component, even if rendered outside the DOM hierarchy of the parent component, has the access to context, props, state and handlers of the parent element. And all events, fired from the inside of portal will bubble up to its ancestor. This makes the handling of modal, dialogs and tooltips more flexible.
Example of Practical Implementation
import React, { useEffect, useState } from "react";
import { createPortal } from "react-dom";
// 'modal-root' is a sibling to 'app-root'
const modalRoot = document.getElementById("modal-root");
function Modal({ isOpen, children }) {
// element to which the modal will be rendered
const el = document.createElement("div");
useEffect(() => {
// append to root when the children of Modal are mounted
modalRoot.appendChild(el);
// do a cleanup
return () => {
modalRoot.removeChild(el);
};
}, [el]);
return (
isOpen &&
createPortal(
// child element
<div
style={{
position: "absolute",
top: 0,
left: 0,
height: "100%",
width: "100%",
padding: "100px",
backgroundColor: "rgba(0,0,0,0.6)"
}}
>
<p
style={{
width: "50%",
background: "white",
padding: "50px",
textAlign: "center"
}}
>
{children}
</p>
</div>,
// target container
el
)
);
}
export default function App() {
const [isModalOpen, setModalOpen] = useState(false);
const toggleModal = () => setModalOpen(!isModalOpen);
return (
<div
style={{
position: "relative",
overflow: "hidden"
}}
>
<button onClick={toggleModal}>open modal</button>
<Modal isOpen={isModalOpen}>
<button onClick={toggleModal}>close modal</button>
</Modal>
</div>
);
}
Top comments (2)
Nice article but there are 2 issues:
export default function Modal
modal-root
const modalRoot = document.getElementById("modal-root");
but it should be just
modal
as this is what you set earlier inindex.html
const modalRoot = document.getElementById("modal");
need ->
const globalDialog = useMemo(
() => document.getElementById(ROOT_DIALOG_ID) as HTMLElement,
[open],
);
const element = useMemo(() => document.createElement('section'), [open]);
prevent possible problems with the states of the other components contained