SEPTEMBER 24, 2024 / #REACT
How to Create a Reusable
Modal Component in React
Grant Riordan
When using React, we strive to create reusable
components as much as we can to limit the number of
components and repetition. This keeps your code
“DRY”.
DRY is a concept you may have come across—it means “Don’t
Repeat Yourself”. DRY is a coding principle that encourages you to
minimize code duplication by using abstractions like functions or
modules.
It's important because it reduces redundancy, makes code easier to
maintain, improves readability, and decreases the risk of errors
during updates.
What Will This Article Cover?
In this article, you’ll learn:
How to build a modal using React and CSS.
How to ensure that the modal can be reused in multiple
scenarios, content and styling.
How to integrate state and callback functions into the modal.
Table of Contents
What Will This Article Cover?
The Core Modal Component
Props Interface
The Markup
React useEffect
When Do We Use useEffect?
How to Use the Reusable Modal
Additional Improvements
Conclusion
The Core Modal Component
In this section, we'll use React to build a component library. There
are multiple patterns that you can follow to do this, but one of my
favorite is the atomic design pattern.
import React, {useEffect} from 'react';
import './[Link]'
interface Props {
open: boolean;
cancelFn?: () => void;
primaryFn?: () => void;
closeIcon?: string;
content?: [Link];
titleContent?: [Link];
className?: string;
}
export const Modal: [Link]<Props> = (props) => {
const {open, cancelFn, primaryFn, closeIcon, titleContent, co
// simple useEffect to capture ESC key to close the modal
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ([Link] === 'Escape' && open) {
if (cancelFn) {
cancelFn();
}
}
};
[Link]('keydown', handleKeyDown);
return () => [Link]('keydown', hand
}, [open, cancelFn]);
if (!open) return null;
return (
<div className="modalBackground">
<div className="modalContainer">
{titleContent && (<div className="title">
{titleContent}
<div className="titleCloseBtn">
<button onClick={cancelFn}>{closeIcon
</div>
</div>
)}
<div className="body">
{content}
</div>
<div className="footer">
{secondaryFn && (
<button onClick={secondaryFn} id="cancelB
Cancel
</button>
)}
{primaryFn && (
<button onClick={primaryFn}>Continue</but
)}
</div>
</div>
</div>
);
};
.modalBackground {
width: 100vw;
height: 100vh;
background-color: rgb(33, 33, 33, 0.9);
position: fixed;
display: flex;
justify-content: center;
align-items: center;
}
.modalContainer {
display: flex;
flex-direction: column;
border-radius: 20px;
background-color: white;
box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px;
.modalContainer .title {
display: flex;
flex-direction: row;
text-align: center;
align-items: center;
justify-content: space-between;
padding: 8px;
border-top-right-radius: 20px;
border-top-left-radius: 20px;
background-color: #FFE936;
}
.titleCloseBtn {
display: flex;
justify-content: flex-end;
}
.titleCloseBtn button {
font-size: 0.3rem;
}
.titleCloseBtn button {
background-color: transparent;
border: none;
font-size: 25px;
cursor: pointer;
}
.modalContainer .body {
flex: 1;
padding: 16px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 1rem;
text-align: center;
}
.modalContainer .footer {
display: flex;
justify-content: center;
align-items: center;
}
.modalContainer .footer button {
width: 150px;
height: 45px;
margin: 10px;
border: none;
background-color: cornflowerblue;
color: white;
border-radius: 8px;
font-size: 20px;
cursor: pointer;
}
#cancelBtn {
background-color: crimson;
}
The code above is the core modal component. Let’s break it down.
Props Interface
interface Props {
open: boolean;
cancelFn?: () => void;
primaryFn?: () => void;
closeIcon?: string | [Link];
content?: [Link];
titleContent?: [Link];
}
In this interface (which we’re passing to the Modal component) we
have:
open : A boolean value that signifies whether the modal
should be shown or not. A common way of toggling the
modal on or off.
cancelFn : An optional parameter (denoted by ? ) that
provides a call back function for when the secondary button
is being pressed. For example, the cancel functionality to
close the modal, or undo an action.
primaryFn : An optional parameter that provides a call back
function for when the primary button is being pressed. For
example, ok , confirm , or submit functionality.
closeIcon : An optional parameter that provides an icon to
be used as the top right close button for the modal. For
example, you could use a circle with an X in it, or another
form of a button.
content : An optional parameter that provides the inner
content for the modal. This could be as simple as a <p/> tag
to a fully fledged <form/> element.
titleContent : An optional parameter that provides content
to be situated within the title section of the modal. This could
be anything from text, to a logo image, anything you want.
The Markup
The markup is pretty straightforward, there are divs for each
section (title, content, and actions) along with some conditional
rendering logic.
That is:
{titleContent && (
<div className="title">
{titleContent}
<div className="titleCloseBtn">
<button onClick={secondaryFn}>{closeIcon ?? 'X'}</but
</div>
</div>
)}
We used the short-circuit evaluation syntax to check if the
titleContent property is defined by the developer. If it is, the
modal’s title is rendered; if not, the title section is omitted.
This approach allows flexible configuration of the modal, letting you
easily include or exclude sections like title, content, or actions.
For example, a confirmation modal might only need a title like 'Are
you sure?' and action buttons like 'Yes' or 'No', without any
additional content.
React useEffect
If you’re not familiar with useEffect and plan on using React more,
l’d highly recommend reading about it here, as it is one of the
backbones of React’s ecosystem.
In essence, useEffect is like a helper that makes sure you do things
at the right time in your app.
When Do We Use useEffect ?
1. When you want something to happen right after your app is
ready:
Example: When the app opens, and you want to fetch
some data from the internet (like loading recipes for
your dinner).
2. When something a state variable or input prop changes, and
you want to do something after that change.
3. When your app closes or cleans up.
In our React App, we’ve created a useEffect Hook that runs after
our modal component has loaded. The useEffect will simply attach
a keydown event handler to the document (the page/DOM), which
will listen to all keys that are pressed on the screen, and then check
if it is the ESC key.
If it is the ESC key, it will call the secondaryFn function passed into
the modal. In our case, this is the function that closes the modal. The
return statement removes the event handler on unmount (when
modalOpen is false ).
How to Use the Reusable Modal
import './[Link]'
import {useState} from "react";
import {Modal} from "./components/molecules/Modal";
function App() {
const [modalOpen, setModalOpen] = useState(false);
return (
<div className="App">
<h1>Hey, click on the button to open the modal.</h1>
<button className="openModalBtn" onClick={() => setMo
Open
</button>
<Modal
open={modalOpen}
titleContent={<h1> Close </h1>}
secondaryFn={() => setModalOpen(false)}
content={
<>
<h2>This is a modal</h2>
<p>You can close it by pressing Escape key,
</>
}
/>
</div>
);
}
export default App
Breaking It Down
In the above code, we have a button component that triggers the
modal to be displayed. This is done by updating the useState
variable modalOpen . Setting this to true will cause the Modal
component to be seen.
Further down the code, we implemented the Modal component and
passed in the relevant properties within the modal: a title, body
content, and a secondary button (we didn't pass a primary function).
This renders the following modal:
Using the same component, we can also mix it up and build a
confirmation modal like so:
Replacing the previous modal implementation with:
<Modal
open={modalOpen}
titleContent={<h1> Are you sure? </h1>}
cancelFn={() => setModalOpen(false)}
primaryFn={() => {
alert(" You deleted everything everything");
setModalOpen(false);
}}
content={
<>
<h4>Do you really want to delete everything?</h4>
</>
}
/>
There you have it, you have a Modal component with endless
possibilities and configurations, depending on what content you
pass to each area of the modal.
Additional Improvements
There are some additional improvements
Replacing the Cancel and Primary Buttons
Instead of passing the cancelFn and primaryFn properties, you can
pass a full component containing the buttons, or any other footer
components.
The updated code should look like this:
import React, { useEffect } from 'react';
import './[Link]';
interface Props {
open: boolean;
escFn: () => void;
closeIcon?: string;
content?: [Link];
titleContent?: [Link];
className?: string;
actions?: [Link]; // This will be used to pass butto
}
export const Modal: [Link]<Props> = (props) => {
const { open, closeIcon, titleContent, content, actions } = p
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ([Link] === 'Escape' && open) {
}
};
[Link]('keydown', handleKeyDown);
return () => [Link]('keydown', hand
}, [open]);
if (!open) return null;
return (
<div className="modalBackground">
<div className="modalContainer">
{titleContent && (
<div className="title">
{titleContent}
<div className="titleCloseBtn">
<button>{closeIcon ?? 'X'}</button>
</div>
</div>
)}
<div className="body">
{content}
</div>
<div className="footer">
{actions && actions}
</div>
</div>
</div>
);
};
Usage:
const handleCancel = () => {
setIsOpen(false);
};
const handleContinue = () => {
[Link]('Continue action');
};
<Modal
open={isOpen}
titleContent={<h2>Confirm Action</h2>}
content={<p>Are you sure you want to proceed?</p>}
closeIcon="X"
actions={
<div className="custom-actions">
<button onClick={handleCancel}>Cancel</button>
<button onClick={handleContinue}>Continue</button>
</div>
}
/>
Here, we’re now passing the buttons as a property. You can also
design the modal to pass the content as a child component, but this
can get messy, as developers may see this at first glance as passing
the modal content, rather than just footer elements.
There are pros and cons of doing it this way though:
Pros:
More flexibility: Allows you to pass all kinds of elements to
the footer section. For example, multiple CTA (Call To Action)
buttons, links, or anything you’d like, with custom styling.
Separation of concerns: The modal is now only responsible
for rendering the container (layout, title, content, and so on).
The logic of what actions (buttons) to display and their
behaviours are handled by the parent component that
renders the modal, which makes the modal component
cleaner and more reusable.
Improved reusability: You can pass any JSX as the actions,
making it usable for a variety of cases (for example, a modal
with form submission buttons or multiple options). This
approach is useful when you have modals that need different
sets of buttons or interactions dependent on other logic
within the parent/modal component. The logic can be
handled by a builder function, or within another wrapper
component which houses the buttons.
Cons:
More responsibility on the parent component: You now
have to handle the buttons in each instance where you use
the Modal . This might result in repetition of the button logic
(like handleCancel and handleContinue ) in different places
if you're not careful.
Slightly more complex usage: The previous approach
allowed you to pass in cancelFn and primaryFn directly
(optionally), which might be easier for the majority/simple
use cases. Passing actions as children may require more
setup.
Inconsistent action layout: If you're not mindful of your
code, you could end up with inconsistent button placement
or styles across different instances of the modal. This can be
managed by ensuring you always pass consistent markup or
styles when passing actions as children, but again, it may
become difficult to manage.
Conclusion
Building a reusable modal component in React offers great
flexibility and reusability across your application. You can easily
adapt the modal to various scenarios, whether it’s a simple
confirmation modal or a more complex form submission modal.
However, it’s essential to balance between flexibility and simplicity
—too much complexity might overburden the parent components
with unnecessary repetition.