DEV Community

Cover image for Simple, Typesafe React Modals using Portals and Custom Hooks
Nate Arnold
Nate Arnold

Posted on

Simple, Typesafe React Modals using Portals and Custom Hooks

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!

Top comments (4)

Collapse
 
franlol profile image
franlol

You can check mine

github.com/franlol/useModal

Collapse
 
arnonate profile image
Nate Arnold

I will check it out, thanks!

Collapse
 
thebox193 profile image
Sir.Nathan (Jonathan Stassen)

Really like this pattern Nate!

Extending off this idea, I've been wondering about making a global Modal provider & context. But I'm not sure how much value it would add.

Collapse
 
arnonate profile image
Nate Arnold

We are driving some of our global UI state with Apollo useReactiveVar now. It's handy and already ships with Apollo. I wouldn't suggest mixing your global state management though, so context works if you are already using it :)