How to create reusable Portal component using React Portal feature

2020-11-116 min
How to create reusable Portal component using React Portal feature

Introduction

Today's article is about creation of reusable Portal component which allows us render some code into a DOM node that exist outside of the parent component.

Our example would be an easy React application (GitHub repo here) which renders Header component within Modal inside.
 Our application job would be to open Modal by clicking button placed inside a Header.

STRUCTURE_2.jpg

What kind of problem can we solve?

Problem that we have in here is actually that all Modal structure would be rendered inside a Header. What I would like to achieve is:

  • render Modal outside a Header
  • have a control of Modal state and content in Header

To solve it we will use React Portal. Please take a look at a diagram. This is exactly what we are going to do.

STRUCTURE_4.jpg

What React documentation says about React Portal?

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

ReactDOM.createPortal(child, container)

The first argument (child) is any renderable React child, such as an element, string, or fragment. The second argument (container) is a DOM element. - React docs

Implementation

I would like to introduce new div element in index.html file and render Modal inside this div. Let's open index.html file and add new div element just below <div id="root" />

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

Then we can create our Portal component starting from new file creation.

./components/Portal/Portal.component.tsx

First we want to define an interface for our component.

interface PortalProps {
  target: string;
}

We use property named target to find a DOM element inside which we would render some node. If we want to have a better control on available targets for our Portal, we can define it precisely using typescript enum.

export enum PortalTarget {
  MODAL = 'modal-portal',
  ROOT = 'root',
}

Let's update PortalProps interface

interface PortalProps {
  target: PortalTarget;
}

Now target props accept only PortalTarget, so we can decide if node element will be rendered inside 'modal-portal' or 'root'. Following this article you will see how am I going to use it.

Since we have types defined, we can create Portal component. In this example I am going to use functional component.

export const Portal: React.FC<PortalProps> = ({ target, children }) => {
  const domElement = document.getElementById(target);

  return domElement
    ? ReactDOM.createPortal(children, domElement)
    : null;
}

Now let's discuss above code. As you can see first of all I created domElement constant which should point to DOM element depends on target we passed in. So passed argument is target props, which has a value equal to 'modal-portal' or 'root'. Secondly we return ReactDOM.createPortal(children, domElement) or null value depends if we found our target as a DOM element.

I really hope that's understandable for you :)

Portal Usage

Since we have a Portal component ready to use, let's now create usage example. I am going to create a Modal component which will be rendered in div with id 'modal-portal'.

import React from 'react';
import { Portal, PortalTarget } from '../Portal/Portal.component';

import './Modal.component.css';

interface ModalProps {
  isOpen: boolean;
  handleClose: () => void;
}

export const Modal: React.FC<ModalProps> = ({ isOpen, handleClose, children }) => {
  const outsideRef = React.useRef(null);

  const closeModal = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
    if (e.target === outsideRef.current) {
      handleClose();
    }
  }

  return isOpen ? (
    <Portal target={PortalTarget.MODAL}>
      <div
        ref={outsideRef}
        className={'modal'}
        onClick={closeModal}
      >
        <div className={'modal__content'}>
          { children }
        </div>
      </div>
    </Portal>
  ) : null;
};

Looking at above component you should notice that I wrapped whole jsx inside our Portal component and I passed PortalTarget.MODAL as a value of target property.

<Portal target={PortalTarget.MODAL}>

It simply means that all what is wrapped by Portal will be moved outside Modal in a DOM tree and rendered inside div with modal-portal id.

Final example

Currently we have a Portal which we can use for a different cases and Modal component. Let's implement usage of Modal within our App.

Now I am going to create a Header component. Maybe you remember what I wrote in the beginning of this article, that I would like to toggle modal from Header, but Modal should be rendered outside a Header. So now is a time to achieve it.

import React from 'react';
import { Modal } from '../Modal/Modal.component';

import './Header.component.css';

export const Header: React.FC = () => {
  const [isModalOpen, setModalState] = React.useState(false);

  const toggleModal = () => setModalState(!isModalOpen);

  return (
    <div className={'header'}>
      <p>LOGO</p>
      <button onClick={toggleModal}>
        open modal
      </button>
      <Modal
        isOpen={isModalOpen}
        handleClose={toggleModal}
      >
        This is content from Header component!
      </Modal>
    </div>
  );
};

Please take a look at code and notice that I am rendering Modal inside a Header component, but because whole Modal content is wrapped inside Portal, all of Modal DOM structure will be moved to target DOM element we defined.

Zrzut ekranu 2020-11-15 o 21.37.04.png

Thank you for reading. I really hope that it would be helpful for you in a real world application.