visit
A Portal can be created usingReactDOM.createPortal(child, container)
. Here the child is a React element, fragment, or string, and the container is the DOM location (node) to which the portal should be injected.
const Modal =({ message, isOpen, onClose, children })=> {
if (!isOpen) return null
return ReactDOM.createPortal(
<div className="modal">
<span className="message">{message}</span>
<button onClick={onClose}>Close</button>
</div>,
domNode)
}
We create our application with
yarn create vite my-modals-app --template react-ts
yarn add styled-components @types/styled-components
src/
├── components/
│ ├── layout/
│ │ ├── Header.tsx
│ │ └── styles.tsx
│ ├── modals/
│ │ ├── Buttons.tsx
│ │ ├── Modal.tsx
│ │ ├── PortalModal.tsx
│ │ ├── index.ts
│ └── └── styles.ts
├── hooks/
│ └── useOnClickOutside.tsx
├── styles/
│ ├── modal.css
│ ├── normalize.css
│ └── theme.ts
├── ts/
│ ├── interfaces/
│ │ └── modal.interface.ts
│ ├── types/
│ └── └── styled.d.ts
├── App.tsx
├── main.tsx
└── config-dummy.ts
App.tsx
: In this component, we have examples of how to use our custom modal. We have buttons that show modals with different configurations to give us an idea of what we can achieve with this modal.
In this component, we also define the theme for our modal, adding a ThemeProvider
and creating a global style with createGlobalStyle
of styled-components
.
import { FC, useState } from "react";
import Header from "./components/layout/Header";
import { Buttons, Modal } from "./components/modals";
import { ThemeProvider } from "styled-components";
import { lightTheme, darkTheme, GlobalStyles } from "./styles/theme";
import * as S from "./components/modals/styles";
import { INITIAL_CONFIG } from "./config-dummy";
import imgModal from "./assets/images/imgModal.jpg";
const App: FC = () => {
const [theme, setTheme] = useState("dark");
const [show1, setShow1] = useState < boolean > false;
const [show2, setShow2] = useState < boolean > false;
const [show3, setShow3] = useState < boolean > false;
const [show4, setShow4] = useState < boolean > false;
const isDarkTheme = theme === "dark";
return (
<ThemeProvider theme={isDarkTheme ? darkTheme : lightTheme}>
<>
<GlobalStyles />
<Header isDarkTheme={isDarkTheme} setTheme={setTheme} />
<main>
<Buttons
show1={show1}
setShow1={setShow1}
show2={show2}
setShow2={setShow2}
show3={show3}
setShow3={setShow3}
show4={show4}
setShow4={setShow4}
/>
<Modal show={show1} setShow={setShow1} config={INITIAL_CONFIG.modal1}>
<h1>My Modal 1</h1>
<p>Reusable Modal with options to customize.</p>
<S.ModalFooter>
<S.ModalButtonSecondary onClick={() => setShow1(!show1)}>
Cancel
</S.ModalButtonSecondary>
<S.ModalButtonPrimary>Acept</S.ModalButtonPrimary>
</S.ModalFooter>
</Modal>
<Modal show={show2} setShow={setShow2} config={INITIAL_CONFIG.modal2}>
<p>Reusable Modal with options to customize.</p>
<input type="email" placeholder="Email" />
<S.ModalFooter>
<S.ModalButtonPrimary>Send</S.ModalButtonPrimary>
</S.ModalFooter>
</Modal>
<Modal show={show3} setShow={setShow3} config={INITIAL_CONFIG.modal3}>
<img src={imgModal} alt="My Modal" />
</Modal>
<Modal show={show4} setShow={setShow4} config={INITIAL_CONFIG.modal4}>
<h1>My Modal 4</h1>
<p>Reusable Modal with options to customize.</p>
</Modal>
</main>
</>
</ThemeProvider>
);
};
export default App;
Modal.tsx
: This component is conditioned to be displayed or not depending on the action performed by the user. It is wrapped in a style component that is superimposed on the screen.
It also receives children, which contains all the content that will be shown inside the modal. It can be any type of tsx
content.
useOnClickOutside
: This is a custom hook that will close the modal when it detects that the user clicks outside the modal.
This hook adds an EventListener
that will respond to the mousedown
and touchstart
event, after this, it will evaluate if the click was inside the element or outside of it.
handleKeyPress
: This is a callback that will be executed when it detects that the user presses the ESC key to close the modal.
It does this by adding an EventListener
to the keydown
event to then evaluate which key was pressed.
import { useCallback, useEffect, useRef } from "react"
import PortalModal from "./PortalModal"
import useOnClickOutside from "../../hooks/useOnClickOutside"
import { ModalConfig } from "../../ts/interfaces/modal.interface"
import * as S from "./styles"
import "../../styles/modal.css"
interface Props {
show: boolean;
config: ModalConfig;
setShow: (value: boolean) => void;
children: JSX.Element | JSX.Element[];
}
const Modal = ({ children, show, setShow, config }: Props) => {
const modalRef = useRef < HTMLDivElement > null
// handle what happens on click outside of modal
const handleClickOutside = () => setShow(false)
// handle what happens on key press
const handleKeyPress = useCallback((event: KeyboardEvent) => {
if (event.key === "Escape") setShow(false)
}, [])
useOnClickOutside(modalRef, handleClickOutside)
useEffect(() => {
if (show) {
// attach the event listener if the modal is shown
document.addEventListener("keydown", handleKeyPress)
// remove the event listener
return () => {
document.removeEventListener("keydown", handleKeyPress)
}
}
}, [handleKeyPress, show])
return (
<>
{show && (
<PortalModal wrapperId="modal-portal">
<S.Overlay
showOverlay={config.showOverlay}
positionX={config.positionX}
positionY={config.positionY}
show={show}
style={{
animationDuration: "400ms",
animationDelay: "0",
}}
>
<S.ModalContainer padding={config.padding} ref={modalRef}>
{config.showHeader && (
<S.ModalHeader>
<h3>{config.title}</h3>
</S.ModalHeader>
)}
<S.Close onClick={() => setShow(!show)}>
<svg
xmlns="//www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
className="bi bi-x"
viewBox="0 0 16 16"
>
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" />
</svg>
</S.Close>
<S.Content>{children}</S.Content>
</S.ModalContainer>
</S.Overlay>
</PortalModal>
)}
</>
)
}
export default Modal
PortalModal.tsx
: This component uses the React Portals, which we have already mentioned previously.
In this component, we use the hook useLayoutEffect
. This hook is a little different from useEffect
since this one is executed when it detects a change in the virtual DOM and not in the state, which is exactly what we are doing when creating a new element in the DOM.
Inside the useLayoutEffect
, we look for and validate if the element has already been created with the id that we have passed, and we set this element. Otherwise, we make a new element in the DOM with the function createWrapperAndAppenToBody
.
Once we have created the element where we are going to insert our modal, we create the portal with createPortal
.
import { useState, useLayoutEffect } from "react";
import { createPortal } from "react-dom";
interface Props {
children: JSX.Element;
wrapperId: string;
}
const PortalModal = ({ children, wrapperId }: Props) => {
const [portalElement, setPortalElement] =
(useState < HTMLElement) | (null > null);
useLayoutEffect(() => {
let element = document.getElementById(wrapperId) as HTMLElement
let portalCreated = false;
// if element is not found with wrapperId or wrapperId is not provided,
// create and append to body
if (!element) {
element = createWrapperAndAppendToBody(wrapperId);
portalCreated = true;
}
setPortalElement(element);
// cleaning up the portal element
return () => {
// delete the programatically created element
if (portalCreated && element.parentNode) {
element.parentNode.removeChild(element);
}
};
}, [wrapperId]);
const createWrapperAndAppendToBody = (elementId: string) => {
const element = document.createElement("div");
element.setAttribute("id", elementId);
document.body.appendChild(element);
return element;
};
// portalElement state will be null on the very first render.
if (!portalElement) return null;
return createPortal(children, portalElement);
};
export default PortalModal;
configDummy.ts
: This is the file we will use as a template to generate different modals, in this case, 4.
import {
ModalConfigDummy,
ModalPositionX,
ModalPositionY,
} from "./ts/interfaces/modal.interface";
export const INITIAL_CONFIG: ModalConfigDummy = {
modal1: {
title: "Modal Header 1",
showHeader: true,
showOverlay: true,
positionX: ModalPositionX.center,
positionY: ModalPositionY.center,
padding: "20px",
},
modal2: {
title: "Modal Header 2",
showHeader: false,
showOverlay: true,
positionX: ModalPositionX.center,
positionY: ModalPositionY.center,
padding: "20px",
},
modal3: {
title: "Modal Header 3",
showHeader: false,
showOverlay: true,
positionX: ModalPositionX.left,
positionY: ModalPositionY.start,
padding: "0",
},
modal4: {
title: "Modal Header 4",
showHeader: false,
showOverlay: true,
positionX: ModalPositionX.right,
positionY: ModalPositionY.end,
padding: "0",
},
};
In this tutorial, we have created a reusable component as we can use anywhere in our application. Using React Portals, we can insert it anywhere in the DOM as it will create a new element with the id
, we assign to it.