Connecting data with Portals in React

Portal allow you to render React components outside the DOM hierarchy. This is very very useful with modals and popovers and tooltips, since, although they are in a different DOM node, React keeps the data connected, and properly binds the event listeners. In this example:

<body>
  <div id="root"></div>
  <div id="modal"></div>
</body>

So, to make this work, I've set up a React component as follows:

const Modal = ({ visible, onClose, children })=> {

  useEffect (()=> {
    if (visible) document.body.classList.add("modal-active")
    else document.body.classList.remove("modal-active")
  }, [visible])

  const handleContentClick = event=> {
    event.preventDefault()
    event.stopPropagation()
  }

  const handleModalClick = ()=> {
    onClose()
  }

  return ReactDOM.createPortal((
    <div className="Modal-backstage" onClick={handleModalClick}>
      <div className="Modal-content" onClick={handleContentClick}>
        {children}
      </div>
    </div>
  ), document.getElementById("modal"))

}

The idea is that this is just the shell, the modal's contents will change depending on what I want to render. Thus, the children property, which is rendered directly. I've also set up some CSS to make it look kind of like a modal. It's not perfect, but it does the job. Note that some of the css are tailored to this site (like the colors).

#modal {
  display: none;
  width: 100%;
  height: 100%;
  margin: 0px;
  position: fixed;
  top: 0px;
  bottom: 0px;
  left: 0px;
  right: 0px;
}

body.modal--active {
  overflow: hidden;
}

body.modal--active #modal {
  display: block;
}

.Modal-backstage {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, .75);
}

.Modal--content {
  color: white;
  border: 1px solid black;
  background: #282c37;
  width: calc(100% - 40px);
  max-width: 750px;
  padding: 10px;
  border-radius: 5px;
  box-shadow: 3px 2px 2px 3px rgba(40, 44, 55, .35);
}

What's interesting here is that, thanks to ReactDOM.createPortal I can capture the onClose event, and update the state accordingly, regardless of the "real" DOM hierarchy. The portal connects the two external nodes, and allows bubbling up the events.

That way, the final parent component, in my case, looks like this:

const View = ()=> {

  const [modal, setModal] = useState(false)

  return (
    <React.Fragment>
      {/* MAIN CONTENT */}
      <Modal
        visible={modal}
        onClose={()=> setModal(false)}
      >
        {/* MODAL CONTENT */}
      </Modal>
    </React.Fragment>
  )
}

You can see it in action right here, right now:

Pretty sweet, huh? Note that, since the modal is children agnostic, you can render pretty much anything inside it. As many React components as you wish. No limits.

Check out the official React documentation for more information on React Portals

More concepts