React Portals by Aditya Tyagi

React Portals – Understanding with examples

A guide to proper placement of DOM Elements in React

While architecting scalable apps, writing syntactically correct React code is not enough. It is important to write semantically correct code as well where React Portals shine. Even if you have nailed the DRY (Don’t Repeat Yourself) coding principle, it is important that you ensure your code follows the best coding practices.

By “Proper Placement” I mean that the final HTML DOM, which is rendered in the browser, should make sense. Let me explain my point using the example of a modal/dialog box. We can follow the example I am taking for tooltips and hovercards.

If you are a developer, you’ll acknowledge the fact that working with modals and dialog boxes can be a painful experience. The overlays & backdrops can test your patience. Let’s say you have a reusable modal functional component and you want to use it in various other components. Making it reusable will help you use the modal within different components with little hassle.

Consider this reusable react functional component for a modal:

Modal.js

import React, { Fragment } from "react";
import classes from "./Modal.module.css";

const Backdrop = (props) => {
  return <div className={classes.backdrop} onClick={props.onClose}></div>;
};
const ModalOverlay = (props) => {
  return (
    <div className={classes.modal}>
      <div className={classes.content}>{props.children}</div>
    </div>
  );
};

const Modal = (props) => {
  return (
    <Fragment>
      <Backdrop onClose={props.onClose} />
      <ModalOverlay>{props.children}</ModalOverlay>
    </Fragment>
  );
};

export default Modal;


Modal.module.css:

.backdrop {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100vh;
    z-index: 20;
    background-color: rgba(0, 0, 0, 0.75);
}

.modal {
    position: fixed;
    top: 20vh;
    left: 5%;
    width: 90%;
    background-color: white;
    padding: 1rem;
    border-radius: 14px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
    z-index: 30;
    animation: slide-down 300ms ease-out forwards;
}

App.js

import React from "react";
import { render } from "react-dom";
import Modal from "./Modal/Modal";

// Functional component
function Parent(props) {
  return (
    <div>
      <h3>This is parent</h3>
      <Modal>
        <h5>I am child in modal</h5>
      </Modal>
    </div>
  );
}

// Main App component
// renders a list of Messages using data from messages.json
const App = (props) => {
  return (
    <div>
      <h2>React Portals by Aditya Tyagi</h2>
      <Parent />
    </div>
  );
};

render(<App />, document.getElementById("root"));

Will be result in:

Modal implementation with and without React Portals
Modal without React Portals

Now, if you closely inspect this in the DOM, you’ll be looking at the modal being rendered within the Parent component.

DOM without React Portals
DOM with Modal rendering within Parent

This arrangement of a modal opening inside a Parent component is not wrong, per se. But it is semantically wrong from a DOM perspective. This is just one Parent. Consider this modal being accessed within a deeply nested Child component.

It will look something like:

Modal implementation in deeply nested child without React Portals
Modal opens in a deeply nested child component

React Portals: The solution

Ideally, the Modal should be placed right next to the root div, just at the start of the body. This should happen without compromising on the ease with which we are using the Modal component within a deeply nested Child component.

Ideal location to render modal
The ideal place to render Modal within DOM

This is where React Portals to rescue.

According to the official documentation:

Portals provide a first-class way to render children into a DOM node that exists outside the DOM hierarchy of the parent component.

Reactjs.org

The API is pretty straight-forward. The method createPortal is present on ReactDom.

ReactDOM.createPortal(child, container)

The method takes two arguments:

child

Anything that you want to render, such as an element, string, or fragment. In our example, we want to render two JSX elements, Backdrop and ModalOverlay. These are the two elements we’ll pass as the 1st argument

container

This is the place wherein you want to render the child element you passed in the 1st argument. You’ll be using the native JavaScript method document.getElementById( ) to target the element wherein you want to render it.

const modalPlaceholderElement = document.getElementById("modal-placeholder");

Therefore, as we want to render the modal and the backdrop right next to the root element, near the body, we’ll be adding a placeholder div there with an id which we’ll use to target it!

Therefore, in index.html

<div id="modal-placeholder"></div>

The API will then look something like:

ReactDom.createPortal(
                <Backdrop onClose={props.onClose} />, // element to render
                modalPlaceholderElement // place to render at
            )

Making the necessary modifications and joining all the above pieces:

I) Index.html

  <body>
    <!--Placeholder for the modal-->
    <div id="modal-placeholder"></div>
    <div id="root"></div>
  </body>

II) Modal.js

import React, { Fragment } from "react";
import classes from "./Modal.module.css";

// Imported ReactDom 
import ReactDom from "react-dom";

// No changes
const Backdrop = (props) => {
  return <div className={classes.backdrop} onClick={props.onClose}></div>;
};

// No changes
const ModalOverlay = (props) => {
  return (
    <div className={classes.modal}>
      <div className={classes.content}>{props.children}</div>
    </div>
  );
};

// Get the modal placeholder div from index.html
const modalPlaceholderElement = document.getElementById("modal-placeholder");

const Modal = (props) => {
  return (
    <Fragment>

      {/* Use createPortal to render the child at the placeholder */}
      {ReactDom.createPortal(
        <Backdrop onClose={props.onClose} />,
        modalPlaceholderElement
      )}

      {/* Use createPortal to render the child at the placeholder */}
      {ReactDom.createPortal(
        <ModalOverlay>{props.children}</ModalOverlay>,
        modalPlaceholderElement
      )}
      
    </Fragment>
  );
};

export default Modal;

This will produce the exact same result, but the DOM will be different:

Modal implementation in deeply nested child with React Portals
Modal is rendering beside root div with React Portals

The same principle applies to tooltips, hovercards and dialog boxes. This will help in creating structurally appropriate and scalable applications. Another way to ensure that your React code is semantically correct is to use React Fragments.

Bonus: A playground

And there you have it. Try to play around and get your hands dirty! I hope you have found this useful. Thank you for reading.

Leave a Reply

Your email address will not be published. Required fields are marked *