Simple, Typesafe React Modals using Portals and Custom Hooks
Tags: react, typescript, modals, hooks
Published: 2020-09-27 20:50:08

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.

```ts
import React from "react";

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

  return [modalIsVisible, toggleModalVisibility] as const;
};
```

## `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.

```ts
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;
};
```

## `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.

```css
.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);
}
```

## `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.

```ts
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}
    />
  )
};
```

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