Simple, Typesafe React Modals using Portals and Custom Hooks
Nate Arnold
Posted on September 27, 2020
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;
};
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 thetoggleVisibility
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 thebody
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}
>
×
</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.
.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.
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!
Posted on September 27, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.