DEV Community

Linas Spukas
Linas Spukas

Posted on

React Portals: Flexible Modal Implementation

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>
  );
}

Latest comments (2)

Collapse
 
ottosamatori profile image
Gregory-Canonne

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

Collapse
 
cathalmacdonnacha profile image
Cathal Mac Donnacha 🚀 • Edited

Nice article but there are 2 issues:

  • You need to export the Modal component:

export default function Modal

  • You reference the id modal-root

const modalRoot = document.getElementById("modal-root");

but it should be just modal as this is what you set earlier in index.html

const modalRoot = document.getElementById("modal");