React Modal Component Implementation

Sinan Bekar
4 min readJan 14, 2022

--

Learn how to build a simple yet powerful modal component. (CRA, Next.js and TypeScript)

Photo by Lautaro Andreani on Unsplash

Modals (also called a modal window, modal popup, modal dialog) display content that temporarily blocks interactions with the main view of the UI. In this tutorial, we will build a simple yet powerful modal component for our React project.

At the end of the tutorial, there will be a live demo.

Let’s get started.

First, we need to create React Portal.

React Portal is a first-class way to render child components into a DOM node outside of the parent DOM hierarchy defined by the component tree hierarchy. The Portal’s most common use cases are when the child components need to visually break out of the parent container like modal boxes, tooltips, hovercards, loaders…

Creating React Portal in CRA

Edit public/index.html and add:

   <body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<--- Add this --->
<div id="modal-root"></div>
</body>

Creating React Portal in Next.js

Create _document.jsx or _document.tsx if you are using TypeScript.

import Document, { Html, Head, Main, NextScript } from 'next/document'class MyDocument extends Document {
static async getInitialProps(context) {
const initialProps = await Document.getInitialProps(context)
return { ...initialProps }
}
render() {
return (
<Html>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocument

For TypeScript, import DocumentContext and type context in getInitialProps so like this: getInitialProps(context: DocumentContext)

Add before </body> :

<div id=”modal-root”></div>

Creating useOnClickOutside hook

We will create useOnClickOutsidehook which accepts React.SetStateActionas a parameter, we will pass the setShowModal state action. This hook will help us to manage outside clicks, for the inside close click, there will be another handle method.

The hook will create a ref inside and process it for click handling then return this ref.

We will attach this ref to the modal and it will check if clicked outside or inside the modal.

There will be another option for re-render or hide & show by CSS display property on close and open. This option is very useful for some scenarios.

JS:

Completed hooks/useOnClickOutside.jsfile:

import React from "react";const useOnClickOutside = (handle) => {
const ref = React.useRef(null);
const handleClickOutside = (e) => {
if (ref.current && !ref.current.contains(e.target)) {
handle(e);
}
};
React.useEffect(() => {
window.addEventListener("click", handleClickOutside, true);
return () => {
window.removeEventListener("click", handleClickOutside, true);
};
});
return ref;
};
export default useOnClickOutside;

TypeScript:

Completed hooks/useOnClickOutside.tsfile:

import React from "react";const useOnClickOutside = (
handle: (
e:
| MouseEvent
| React.MouseEvent<HTMLElement>
) => void) => {

const ref = React.useRef<HTMLInputElement>(null);
const handleClickOutside = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) {
handle(e);
}
};
React.useEffect(() => {
window.addEventListener("click", handleClickOutside, true);
return () => {
window.removeEventListener("click", handleClickOutside, true);
};
});
return ref;
};
export default useOnClickOutside;

Modal Component

Create components/modal.js or tsx. Create modal.css.

Import React, ReactDom, useOnClickOutside hook and modal.css to modal.js

import React from "react";
import ReactDOM from "react-dom";
import useOnClickOutside from "../hooks/useOnClickOutside";
import "../modal.css";

Create modal const, showModal will be our state and setShowModal will be our setStateAction. title and reRenderOnCloseOpen are will be optional.

const Modal = ({
showModal,
setShowModal,
children,
title = "",
reRenderOnCloseOpen = true
}) => {};

handleCloseClick will be used both on the close button and the close outside hook.

const handleCloseClick = (e) => {
e.preventDefault();
setShowModal(false);
};
const ref = useOnClickOutside(handleCloseClick);

We will attach the ref object to modal-main instead of modal-container.

const modalContent = (showModal || !reRenderOnCloseOpen) && (
<div
className={`modal-container ${
!showModal && !reRenderOnCloseOpen ? "hidden" : ""
}
`}
>
<div ref={ref} className="modal-main">
<div className="relative">
<div className="absolute right-0">
<button onClick={handleCloseClick}>x</button>
</div>
</div>
<div>
<div>{title}</div>
<div>{children}</div>
</div>
</div>
</div>
);

At the end of the code, we need to return with ReactPortal .

For CRA:

return ReactDOM.createPortal(
modalContent,
document.getElementById("modal-root")
);

For Next.js: We need to check if the window is attached to the DOM because of the hydration progress.

if (typeof window !== "undefined") {
return ReactDOM.createPortal(
modalContent,
document.getElementById("modal-root")
);
} else {
return null;
}

Completed Modal Component

import React from "react";
import ReactDOM from "react-dom";
import useOnClickOutside from "../hooks/useOnClickOutside";
import "../modal.css";
const Modal = ({
showModal,
setShowModal,
children,
title = "",
reRenderOnCloseOpen = true
}) => {
const handleCloseClick = (e) => {
e.preventDefault();
setShowModal(false);
};
const ref = useOnClickOutside(handleCloseClick);
const modalContent = (showModal || !reRenderOnCloseOpen) && (
<div
className={`modal-container ${
!showModal && !reRenderOnCloseOpen ? "hidden" : ""
}
`}
>
<div ref={ref} className="modal-main">
<div className="relative">
<div className="absolute right-0">
<button onClick={handleCloseClick}>x</button>
</div>
</div>
<div>
<div>{title}</div>
<div>{children}</div>
</div>
</div>
</div>
);
return ReactDOM.createPortal(
modalContent,
document.getElementById("modal-root")
);
};
export default Modal;

For the Typescript:

Add Props interface:

interface Props {
showModal: boolean;
setShowModal: React.Dispatch<React.SetStateAction<boolean>>;
title?: string;
reRenderOnCloseOpen?: boolean;
}

Change const Modal = to const Modal: React.FC<Props> =

Add type to handleCloseClick’s e parameter (e: React.MouseEvent<HTMLElement> | MouseEvent)

Also do not forget to check hydration when returning the portal object for Next.js.

Styling

Edit the modal.css and add these:

.modal-container {
position: fixed;
top: 0;
width: 100%;
height: 100%;
z-index: 999;
display: flex;
justify-content: center;
align-items: center;
}
.modal-main {
width: 18rem;
height: 20rem;
border-radius: 0.5rem;
padding: 1rem;
background-color: #464646;
color: white;
}
.flex {
display: flex;
}
.hidden {
display: none;
}
.right-0 {
right: 0;
}
.relative {
position: relative;
}
.absolute {
position: absolute;
}

Usage:

import Modal from "./components/Modal";
import React from "react";
export default function App() {
const [showModal, setShowModal] = React.useState(false);
return (
<div className="App">
<button
onClick={() => {
setShowModal(true);
}}
>
Open Modal
</button>
<Modal
title="Example Modal"
showModal={showModal}
setShowModal={setShowModal}
>
<div>ModalContent</div>
</Modal>
</div>
);
}

Live Demo: https://codesandbox.io/s/react-modal-8zsxk

Final Words

In this tutorial, we implemented a simple yet powerful Modal. You can improve it as you want if you get the main idea.

You can check my GitHub if you want to see my other works.

https://github.com/sinanbekar

--

--