Using React and Typescript to Create Reusable and Customizable Modals

Written by ljaviertovar | Published 2022/12/19
Tech Story Tags: reactjs | typescript | react-tutorial | typescript-tutorial | styled-components | front-end-development | web-development | javascript

TLDRIn this tutorial, you will learn how to develop and implement a modal component in your React project with TypeScript. It will be reusable in any part of your application, and you will be able to customize it and add any type of content. We will use the Portals component to render children outside the root DOM node.via the TL;DR App

As a front-end developer, you have probably had to build a modal window on more than one occasion. This type of element differs from the typical pop-ups because it does not appear automatically, instead, the user has to click somewhere on the website (usually a button) to make it appear.

In this tutorial, you will learn how to develop and implement a modal component in your React project with TypeScript. It will be reusable in any part of your application, and you will be able to customize it and add any type of content.

What Are Modal Windows?

Modals are undoubtedly one of the most used components on the web because they can be used in different contexts, from messages to user input. They have placed an overlay on the screen. Therefore, they have visual precedence over all other elements.

Like many other components in React, a dependency can be installed to help in this process. However, we always end up limited in several aspects, and one of them is styling.

We can create a modal inside or outside the element we call it from in the DOM hierarchy, but to fulfill the definition of a modal, it should be at the same level as the element used as root in React, and to achieve this, we will use the Portals.

What Are Portals in React?

Portals provide a quick and easy way to render children to a DOM node that exists outside the DOM hierarchy of the parent component.

In React, the default behavior is to render the entire application under a single DOM node — the root of the application, but what if we want to render children outside the root DOM node? And you want children to appear visually on top of its container.

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.

Below is an example of a modal component created using the above API.

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)
}

Although a Portal is rendered outside the parent DOM element, it behaves similarly to a normal React component within the application. It can access props and the context API.

This is because the Portals reside within the React Tree hierarchy, and Portals only affect the HTML DOM structure and do not impact the React component tree.

Developing Modals in React

Setting up

We create our application with vite with the following command:

yarn create vite my-modals-app --template react-ts

We install the dependencies that we will need in the project:

yarn add styled-components @types/styled-components

After that, we create the following structure for the project:

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

Components

As we can see in the folder structure, we have several functional and styling components for this app, but in order not to make this tutorial long, we will focus only on the main components.

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.

This component receives as property the configuration that is where we will define how our modal will be seen, that is to say, the position where it will be shown, the title of the modal, paddings, etc.

It also receives children, which contains all the content that will be shown inside the modal. It can be any type of tsxcontent.

Also, in this component, we have a couple of functionalities, which serve us to close the modal.

useOnClickOutside: This is a custom hook that will close the modal when it detects that the user clicks outside the modal.

This hook receives as a parameter the reference of the element that we want to detect and a callback that is the action that we want to make when detecting a click.

This hook adds an EventListenerthat will respond to the mousedownand 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 keydownevent 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="http://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.

It receives children that would be our modal and an id that we will use to assign it to an HTML element.

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.

With this function, we can create the element where it best suits us. In this case, it is being created at the same level as the root element within the body.

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.

As you can see, you can make a lot of combinations to generate a modal different from each other, and you could add more configurations if you wish.

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",
  },
};

That’s it! we have our cool Modal.

See the demo here.

Repo here

Conclusion

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.

We also have different styling options for our modal, and we can add the ones we can think of, besides having implemented a dark mode that I particularly like.

I hope this tutorial has been useful for you and that you have learned new things in developing this application.


Also published here.


Written by ljaviertovar | ☕Web Developer |🤖Automatization and Web Scraping enthusiast |🚀Lover of sci-fi
Published by HackerNoon on 2022/12/19