🚀 Building Scalable React Modal Component with Custom Hook 🛠️ [TailwindCSS, DaisyUI, and TypeScript]

Prithviraj Mazumder
TechVerito
Published in
6 min readJan 15, 2024
Photo by Lautaro Andreani on Unsplash

As front-end developers, we often find ourselves grappling with the challenge of creating reusable and scalable UI components. Consider this an attempt to create a reusable <Modal /> component for React.

TLDR 👀

If you are a PRO and only interested in implementation then in this Github Repository you can find the code.

Inspiration

I was onboarded on a project where I was assigned to build a component library from scratch.
Modal component was one of them,
and TailwindCSS and DaisyUI was chosen because those were aligning with our project’s styling needs.

Problem 🤔

While browsing DaisyUI’s Modal documentation, I was wondering how to create a reusable React component without…

const [isModalOpen, setIsModalOpen] = useState<boolean>(false)

return (
<>
<Modal isOpen={isModalOpen}>
...modal content
<button onClick({() => setIsModalOpen(false)})>
Okay
</button>
</Modal>

<button onClick({() => setIsModalOpen(true)})>
Open Dialog
</button>
</>
)

maintaining a state for opening and closing.

As we can see, the problem 🤔 is that we have to maintain a state [isModalOpen] for displaying the modal every time where we want to use it. 🔄

This solution is not:

  • Clean ❌
  • Readable ❌
  • Developer friendly ❌

Solution

All of these code are in this Github Repository

  1. At first, we will create a Custom <Modal /> Component which will contain the:
    + HTML Dialog element
    + The backdrop for the modal
    + And all the necessary styles

2. Then we will introduce a Custom useModal Hook which builds this Modal component with necessary props 🛠️ and will provide functions that will toggle the Popup/Dialog in the UI ✨

Let’s build the <Modal /> Component

Here is the code of the component

import { forwardRef, ReactNode } from 'react'

export type ModalProps = {
children?: ReactNode
onBackdropClick?: () => void
modalBoxClassName?: string
// you can add more classNames as per your level of customisation needs
}

export const Modal = forwardRef<HTMLDialogElement, ModalProps>(
({ children, modalBoxClassName, onBackdropClick }, ref) => {
return (
<dialog ref={ref} className="modal">
<div className={`modal-box ${modalBoxClassName ?? ''}`}>{children}</div>
<form method="dialog" className="modal-backdrop">
<button
type="button"
onClick={() => {
onBackdropClick && onBackdropClick()
}}
>
close
</button>
</form>
</dialog>
)
}
)

Props

  1. children: this will be used to render content inside the modal
  2. ref: (important) It will take an HTMLDialogRef to toggle the modal open state
  3. onBackdropClick: (optional) will take a function in case we need to close the modal on click of the modal backdrop
  4. modalBoxClassName: (optional) will take CSS classes to modify the styles in case we need a different style for the modal container.

Usage

If we want to use this component without the hook then we have to declare a ref [with a type of <HTMLDialogElement>] everywhere we want to use this component

const modalRef = useRef<HTMLDialogElement>(null)

return (
<div>
<Modal ref={modalRef}>
<h1 className="text-2xl font-bold">I am your modal's title</h1>
<div className="py-4">
<p className="text-lg">I am you modal's content</p>
</div>
<div className="flex items-center w-full">
<button className="btn ml-auto" onClick={() => modalRef?.current?.close()}>
Close
</button>
</div>
</Modal>
<button className="btn ml-auto" onClick={() => modalRef?.current?.showModal()}>
Close
</button>
</div>
)

Declaring a ref 👆 every time is a pain, So let’s improve this implementation!

Let’s make it better with useModal custom hook

Usage flow while using useModal hook

Instead of directly using the <Modal /> Component we will now access it via useModal Custom Hook

import { ReactNode, useRef } from 'react'
import { Modal, ModalProps } from '../components/Modal.tsx'

export type UseModalResp = {
modal: ReactNode
closeModal: () => void
openModal: () => void
modalBoxClassName?: string
}

export type UseModalProps = Omit<ModalProps, 'onBackdropClick'> & {
shouldAllowBackdropClick?: boolean //if it is true then modal can be closed
onModalOpen?: () => void //this function will be called on calling of openModal
onModalClose?: () => void //this function will be called on calling of closeModal
}

export const useModal = ({
children,
modalBoxClassName,
shouldAllowBackdropClick = true,
onModalClose,
onModalOpen
}: UseModalProps): UseModalResp => {
const ref = useRef<HTMLDialogElement | null>(null)

const closeModal = () => {
onModalClose && onModalClose()
ref.current?.close()
}

const openModal = () => {
onModalOpen && onModalOpen()
ref.current?.showModal()
}

const modal: ReactNode = (
<Modal
onBackdropClick={() => {
if (shouldAllowBackdropClick) {
closeModal()
}
}}
ref={ref}
modalBoxClassName={modalBoxClassName}
>
{children}
</Modal>
)

return {
closeModal,
openModal,
modal
}
}

Props

  • children: this will be used to render content inside the modal
  • modalBoxClassName: (optional) will take CSS classes to modify the styles in case we need a different style for the modal container.
  • shouldAllowBackdropClick: (optional) If passed then this flag determines whether the modal will close on click of backdrop
  • onModalOpen: (optional) If passed then this function will be called while opening the modal
  • onModalClose: (optional) If passed then this function will be called while closing the modal

Returns

  1. modalComponent: this will contain the complete modal with children passed to it that we wanted to render.
  2. openModal: this is the function that we will call to open the modal
  3. close modal: this is the function that we will call to close the modal

Why do we need this hook? 🪝

const App = () => {
const { modal, openModal, closeModal } = useModal({
children: (
<>
<h1 className="text-2xl font-bold">I am your modal's</h1>
<div className="py-4">
<p className="text-lg">I am your modal's content</p>
</div>
<div className="flex items-center w-full">
<button className="btn ml-auto" onClick={() => closeModal() //closing the modal}>
Close
</button>
</div>
</>
)
})

return (
<div className="bg-base-100 w-[100svw] h-[100svh] flex flex-col items-center justify-center">
{modal} {/*. <---- rendering the modal component*/}
<button className="btn btn-primary" onClick={() => openModal() //opening the modal}>
Open Modal
</button>
</div>
)
}
A working example of the modal component using useModal hook

This solution is

  • Clean ✅
  • Readable ✅
  • Developer friendly ✅
  1. We don’t need to maintain a state whenever we want to use a modal
  2. Neither we need to use a ref to show and close a modal

If we don’t want to add JSX above the return statement then we can extract it to a different component

<>
<h1 className="text-2xl font-bold">I am your modal's</h1>
<div className="py-4">
<p className="text-lg">I am your modal's content</p>
</div>
<div className="flex items-center w-full">
<button className="btn ml-auto" onClick={() => closeModal() //closing the modal}>
Close
</button>
</div>
</>

Then our code will look like this 👇

const App = () => {
const { modal, openModal, closeModal } = useModal({
children: <MyDummyModal closeModal={() => closeModal} />
})

return (
<div className="bg-base-100 w-[100svw] h-[100svh] flex flex-col items-center justify-center">
{modal} {/*rendering the modal component*/}
<button className="btn btn-primary" onClick={() => openModal()}>
Open Modal
</button>
</div>
)
}

And voilà! With just a few lines of code, your modal is ready. The Component illustrates how effortlessly you can integrate this modal into your project 👍.

Wrapping Up

Building scalable and easy-to-use components is every developer’s dream. Feel free to use and tweak them in your projects, and remember happy coding, React developers! 😎✨

Feedback ✉️

Please let me know if you have some suggestions on it and feel free to fork it from this repo and change it however you want.

--

--

Prithviraj Mazumder
TechVerito

A Senior Consultant | Full-stack Engineer @TechVerito, developing robust and scalable softwares, and clean code for my team