DEV Community

loading...
Cover image for Simple, Typesafe React Modals using Portals and Custom Hooks

Simple, Typesafe React Modals using Portals and Custom Hooks

arnonate profile image Nate Arnold ・3 min read

Modals, for better or worse are an often requested feature in web applications. I recently ran across a pattern that allows for managing modal state and placement in a React application that not only works, but feels OK to implement. The use of a custom hook allows the management of modal state without relying on a state management lib and without polluting your component or application state. React Portals allow us to attach components anywhere we want in our application. In this example, we will hoist the component completely out of our component's parent scope and append it to the body element.

useModal.ts

useModal.ts is a custom hook that manages the visibility of our modal. The hook returns the visibility of the modal and a toggleVisibility function that does exactly what the name implies.

import React from "react";

export const useModal = () => {
  const [modalIsVisible, setModalIsVisible] = React.useState(false);
  const toggleModalVisibility = () => setModalIsVisible(!modalIsVisible);

  return [modalIsVisible, toggleModalVisibility] as const;
};
Enter fullscreen mode Exit fullscreen mode

Modal.tsx

Modal.tsx is the modal component. Notes:

  • The custom useModal hook gives us access to the state of the modal from within the modal itself and allows us to toggle visibility by passing the toggleVisibility function into our modal UI.
  • ReactDOM.createPortal allows us to hoist the modal component outside of the scope of it's parent node and attach it to the body of our application.
import React from "react";
import ReactDOM from "react-dom";

type ModalProps = {
  isVisible: boolean;
  toggleVisibility: () => void;
  modalContent: React.ReactNode;
};

export const Modal = ({
  isVisible,
  toggleVisibility,

}: Readonly<ModalProps>): JSX.Element | null => {
  const modal: JSX.Element = (
    <>
      <div className="backdrop" onClick={toggleVisibility} />
      <div className="modal" aria-modal aria-label="Modal Details" role="dialog">
        {modalContent}

        <span
          className="modal-close"
          aria-label="Close Modal Details"
          onClick={toggleVisibility}
        >
          &times;
        </span>
      </div>
    </>
  );

  return isVisible ? ReactDOM.createPortal(modal, document.body) : null;
};
Enter fullscreen mode Exit fullscreen mode

modal-styles.css

CSS is needed to display the modal correctly. Styles will be incredibly application-dependent, but I usually start with some fixed positioning and a close button in the top right corner.

.backdrop {
  background-color: rgba(255, 255, 255, 0.6);
  bottom: 0;
  left: 0;
  position: fixed;
  right: 0;
  top: 0;
}

.modal {
  --var-rhythm: 1.8rem;

  align-items: center;
  background-color: white;
  border: 1px solid gray;
  border-radius: 6px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  left: 50%;
  max-width: calc(100vw - var(--rhythm));
  max-height: calc(100vh - var(--rhythm));
  min-width: 300px;
  padding: calc(var(--rhythm) * 2) calc(var(--rhythm) * 2) var(--rhythm);
  position: fixed;
  text-align: center;
  top: 50%;
  transform: translate(-50%, -50%);
  overflow-y: scroll;
}

@media (min-width: 600px) {
  .modal {
    min-width: 600px;
  }
}

.modal > * {
  margin: 0;
  margin-bottom: var(--rhythm);
}

.modal-close {
  color: gray;
  cursor: pointer;
  font-size: 2rem;
  line-height: 1rem;
  padding: 0;
  position: absolute;
  right: calc(var(--rhythm) / 2);
  top: calc(var(--rhythm) / 2);
}
Enter fullscreen mode Exit fullscreen mode

Component.tsx

Now, all that is needed to use our modal is to import the hook and Modal.tsx anywhere we need it in our application.

import { Modal } from "../components/Modal";
import { useModal } from "../../hooks/useModal";

export const Component = (): JSX.Element => {
  const [modalIsVisible, toggleModalVisibility] = useModal();
  const modalContent: React.ReactNode = (<p>This goes in the modal.</p>);

  return (
    <Modal
      isVisible={modalIsVisible}
      toggleVisibility={toggleModalVisibility}
      modalContent={modalContent}
    />
  )
};
Enter fullscreen mode Exit fullscreen mode

Have fun making modals ಠ_ಠ! If you have a better pattern for implementing them I would love to be schooled... keep learning!

Discussion (2)

pic
Editor guide
Collapse
franlol profile image
Collapse
arnonate profile image
Nate Arnold Author

I will check it out, thanks!