How to build a reusable modal component with react-modal

Mathew Pregasen
Mathew Pregasen
Guest Writer

Aug 1, 2024

If you’ve ever built an app or website, you probably needed to use a modal. Modals are UI patterns that provide a clean and efficient way to present and collect information without disrupting the overall flow of an application. They’re often used for tasks such as displaying forms, alerts, and confirmations, alongside other temporary user interactions.

Modals can be built via React components and some light logic. And with libraries like `react-modal`, which we’ll be working with here, building modals can be easier. They do a lot of the heavy lifting for us so we can focus on building a reusable modal, rather than the intricacies of writing one from scratch.

However, react-modal is mostly concerned with tracking events to close a modal (such as Escape)—so in this tutorial, we’ll cover creating a personalizable wrapper component that’ll make react-modal more useful.

In this tutorial, we’ll learn how to:

  • Build a React wrapper component that extends the react-modal library
  • Customize the wrapper component’s logic
  • Make the component reusable for consistency across the app
  • Rig actions to the underlying modal

This tutorial is strictly for the react-modal library. While some of the principles could be applied to other libraries, they’ll mostly be pertinent to applications that want to use the react-modal package.

Notably, we will not be covering styling modals. Styling React components is a fairly opinionated topic from both a design and technical standpoint. For the purposes of this post, we’ll just focus on the functional aspects of building a reusable modal component.

What are modals and why should you use them?

Even if this is your first time building a modal, you’ve almost certainly interacted with them in the past. Modals enable applications to replace dedicated, bulky pages with smaller, compartmentalized views to provide a more streamlined and efficient interaction.

Modals are incredibly versatile and found across different types of applications. Here are a few examples of modals you may have encountered:

  • User authentication and registration: Modals are often used to display login and registration forms. In these instances, when a user clicks a “Log In” or “Sign Up” button, a modal appears with a form for their credentials.
  • Contextual help and tooltips: In some applications, when a user clicks on a help icon or a “Learn More” link, a modal appears with detailed explanations about the feature or concept.
  • Confirmation and alerts: Modals are handy for confirmations and alerts. For example, when a user attempts to delete something, a modal dialog can ensure the user’s intent by asking them to “Confirm” or “Cancel.”

Because we'll build a modal as any other React component, they have the same capacity to seamlessly transfer data between users and the application—with the bonus of not leaving the page!

From a developer’s standpoint, modals are often easier to implement and maintain when compared against separate pages. Modals don’t require routing and can be easily integrated into existing components, making them suitable for various interactions without creating complex page structures.

Using react-modal to build a simple React modal

Let’s begin with some basic housekeeping. If you haven’t done so already, you’ll want to create a React app using a package manager, such as npm or yarn:

npx create-react-app yourappname

Next, you’ll want to install the react-modal library:

npm install react-modal

Now that we’ve created an app and installed the react-modal library, let’s set-up a simple React modal component that can be toggled via a button.

The parts of a React modal

There are four basic parts that make up a React modal:

  • A modal container. Often, this is referred to as an overlay.
  • A header, which displays the title and often an x-out / close icon.
  • A body, which contains the content.
  • A footer, which includes action buttons.

These are the main features found in modals, but it's not a fixed list. For instance, some modals may have no action buttons, and therefore, no footer.

Creating a modal file

Let’s begin with a new file named Modal.js in your project’s src directory. We’ll import a few things—(i) the react-modal library and (ii) the useState, a built-in hook in React that will help you manage and update the modal’s state

import Modal from 'react-modal';

import React, { useState } from 'react';

Next, we’ll instantiate React state alongside a helper function, closeModal, to dismiss the modal.

1const AppModal = () => {
2
3
4
5const [isModalOpen, setIsModalOpen] = useState(true);
6
7
8const AppModal = () => {
9
10	const [isModalOpen, setIsModalOpen] = useState(true);
11
12	const closeModal = () => {
13		setIsModalOpen(false);
14	};
15
16}
17
18export default AppModal;
19

These functions will allow you to easily rig user actions to opening and closing the modal.

Next, we’ll utilize the react-modal library to render the modal. This will utilize the Modal component from the library, which gets mounted directly inside our return JSX. Modal has a required isOpen prop that we’ll use to control its visibility. Additionally, we’ll use the optional onRequestClose prop to close the modal (the react-modal ****library calls onRequestClose ****automatically when the user clicks outside the modal or presses the Escape key).

1const AppModal = () => {
2
3	const [isModalOpen, setIsModalOpen] = useState(true);
4
5	const closeModal = () => {
6		setIsModalOpen(false);
7	};
8
9	return (
10		<div>
11			<Modal
12				isOpen={isModalOpen}
13				onRequestClose={closeModal}
14				contentLabel="Example Modal"
15			>
16				<h2>Title</h2>
17				<p>This is a modal.</p>
18				<button onClick={closeModal}>Close Modal</button>
19			</Modal>
20		</div>
21	);
22}
23
24export default AppModal; 

These mechanisms allow for a smooth and user-friendly experience to manage the modal’s visibility without forcing you to deal with native browser events.

Using the above code, you’ll end up with a basic React modal that looks like this:

(Glamorous, right?)

Reusability

Of course, we don’t want to create a new modal component for every use case of a modal. Rather, the point is to create a reusable component that can be dynamically modified via React props.

For instance, the modal’s title and description should be set by the container application. We can accomplish this with some simple props.

1const AppModal = (props) => {
2
3	const [isModalOpen, setIsModalOpen] = useState(true);
4
5	const closeModal = () => {
6		setIsModalOpen(false);
7	};
8
9	return (
10		<div>
11			<Modal
12				isOpen={isModalOpen}
13				onRequestClose={closeModal}
14				contentLabel="Example Modal"
15			>
16				{props.title && <h2>{props.title}</h2>}
17				<p>{props.description}</p>
18				<button onClick={closeModal}>Close Modal</button>
19			</Modal>
20		</div>
21	);
22}
23
24export default AppModal; 
25

In this code snippet, we’ve set the title and description dynamically, and also made the title functionally optional by removing the h2 element if it isn’t defined.

Now, we can call AppModal from any other component and set it dynamically.

1const AppModal = (props) => {
2
3	const [isModalOpen, setIsModalOpen] = useState(true); 
4
5	const closeModal = () => {
6		setIsModalOpen(false);
7	};
8
9	return (
10		<div>
11			<Modal
12				isOpen={isModalOpen}
13				onRequestClose={closeModal}
14				contentLabel="Example Modal"
15			>
16				{props.title && <h2>{props.title}</h2>}
17				<p>{props.description}</p>
18				<button onClick={closeModal}>Close Modal</button>
19			</Modal>
20		</div>
21	);
22}
23
24const Example = () => {
25	return <AppModal
26		title="My Title!"
27		description="My Description!"
28	></AppModal> 
29}
30
31export default AppModal;

Now, our component is entirely reusable! However, instead of description, the modal’s interior should be any arbitrary JSX. This could allow for more interesting interiors, including dynamic components.

To fix this, we’ll just change it to props.children with no p tag.

1const AppModal = (props) => {
2
3	const [isModalOpen, setIsModalOpen] = useState(true);
4
5	const closeModal = () => {
6		setIsModalOpen(false);
7	};
8
9	return (
10		<div>
11			<Modal
12				isOpen={isModalOpen}
13				onRequestClose={closeModal}
14				contentLabel="Example Modal"
15			>
16				{props.title && <h2>{props.title}</h2>}
17				{props.children}
18				<button onClick={closeModal}>Close Modal</button>
19			</Modal>
20		</div>
21	);
22}
23
24const Example = () => {
25	return <AppModal
26		title={"My Title!"}
27		description={"My Description!"}
28	>
29<div>Children content!</div>
30</AppModal> 
31}
32
33export default AppModal; 

The enigma of state

In the previous example, there’s a problem! The parent component (Example) has no way to determine if the modal was closed. It technically knows when the modal was opened, as the modal is default open once it mounts. But generally speaking, the parent component is presently blind to the modal’s state.

This presents an issue. A parent component might need to know if a modal was closed. It might be able to infer this via any state variables controlled by props.children, but that’s an inelegant solution that creates edge cases.

While there are many styles to tackle this, including managing a modal’s state externally, we can pursue a simpler strategy. The modal could remain default open, and simply call an (optional) function whenever it is closed.

1const AppModal = (props) => {
2
3	const [isModalOpen, setIsModalOpen] = useState(true);
4
5	const closeModal = () => {
6		setIsModalOpen(false);
7		props.onClose?.();
8	};
9
10	return (
11		<div>
12			<Modal
13				isOpen={isModalOpen}
14				onRequestClose={closeModal}
15				contentLabel="Example Modal"
16			>
17				{props.title && <h2>{props.title}</h2>}
18				{props.children}
19				<button onClick={closeModal}>Close Modal</button>
20			</Modal>
21		</div>
22	);
23}
24
25const Example = () => {
26	return <AppModal
27		title={"My Title!"}
28		description={"My Description!"}
29		onClose={() => { /* do anything! */ }}
30	></AppModal> 
31}
32
33export default AppModal; 

Now, the parent component can determine if a modal has been dismissed!

Adding actions

As previously mentioned, many modals have footers with action buttons. In our current set up, this is already possible if those action buttons were part of the props.children.

However, it’s convenient if a modal already has pre-built buttons. While there are many techniques for adding buttons—some more opinionated than others—we’ll stick to enabling up to two (optional) buttons: a positive button (blue) and a negative button (red). These are in addition to the pre-existing close button. This should address a majority of use cases while still allowing for a custom footer via props.children.

1const AppModal = (props) => {
2
3	const [isModalOpen, setIsModalOpen] = useState(true);
4
5	const closeModal = () => {
6		setIsModalOpen(false);
7		props.onClose && props.onClose(); 
8	};
9
10	return (
11		<div>
12			<Modal
13				isOpen={isModalOpen}
14				onRequestClose={closeModal}
15				contentLabel="Example Modal"
16			>
17				{props.title && <h2>{props.title}</h2>}
18				{props.children}
19				<div>
20					{props.positiveAction && <button 
21						onClick={props.positiveAction}
22						style={{background: "blue"}}
23					>
24						{props.positiveActionLabel || "Yes"} 
25					</button>}
26					{props.negativeAction && <button 
27						onClick={props.negativeAction}
28						style={{background: "red"}}
29					>
30						{props.negativeActionLabel || "No"} 
31					</button>}
32					<button onClick={closeModal}>Close Modal</button>
33				</div>
34			</Modal>
35		</div>
36	);
37}
38
39const Example = () => {
40	return <AppModal
41		title={"My Title!"}
42		description={"My Description!"}
43		onClose={() => { /* do anything! */ }}
44		positiveAction={() => { /* do anything ! */ }}
45		positiveActionLabel={"Do it!"}
46	></AppModal> 
47}
48
49export default AppModal; 

These props are entirely optional (as are the labels). Now, developers can easily rig actions to modals without having to embed them in the modal’s body. And while this tutorial does not cover styling components, this will force more consistent styling across modals.

Importing your component elsewhere

Of course, you’ll want to utilize your component throughout your application. We can call it in any file by importing it:

1import AppModal from "/path/to/AppModal.js";
2
3const Main () => {
4	<AppModal
5		title={"My Title!"}
6		description={"My Description!"}
7		onClose={() => { /* do anything! */ }}
8		positiveAction={() => { /* do anything ! */ }}
9		positiveActionLabel={"Do it!"}
10	></AppModal> 
11}

That’s it!

Start building reusable modal components

In this tutorial, we covered how to create a modal component that can be reused across projects. For more advanced tutorials, you can check out the examples provided in react-modal’s documentation.

If you’re looking to move a little faster, Retool provides pre-built React components like the React modal so you can quickly build and customize apps. Check out our Component library or docs for more info, or start building for free on Retool today.

Reader

Mathew Pregasen
Mathew Pregasen
Guest Writer
Aug 1, 2024
Copied