🚀 Building Scalable React Modal Component with Custom Hook 🛠️ [TailwindCSS, DaisyUI, and TypeScript]
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
- 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
- children: this will be used to render content inside the modal
- ref: (important) It will take an HTMLDialogRef to toggle the modal open state
- onBackdropClick: (optional) will take a function in case we need to close the modal on click of the modal backdrop
- 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
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
- modalComponent: this will contain the complete modal with children passed to it that we wanted to render.
- openModal: this is the function that we will call to open the modal
- 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>
)
}
This solution is
- Clean ✅
- Readable ✅
- Developer friendly ✅
- We don’t need to maintain a state whenever we want to use a modal
- 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.