How To Make Confirmation Modal/Prompt with React

May 22, 2023

3 min read

How To Make Confirmation Modal/Prompt with React
Watch on YouTube

Including a confirmation prompt when users engage in potentially destructive actions is a widely accepted practice. For example, Increaser effectively employs a modal to present a confirmation prompt when users wish to permanently delete a project. As such scenarios can arise in various projects, I'd like to share an abstraction I use to seamlessly incorporate such prompts.

example

Here's the code from Increaser for delete project functionality that leverages two abstract components - Opener and ConfirmationModal.

<Opener
  renderOpener={({ onOpen }) => (
    <IconButton kind="alert" icon={<TrashBinIcon />} onClick={onOpen} />
  )}
  renderContent={({ onClose }) => (
    <ConfirmationModal
      title="Delete project"
      onClose={onClose}
      confirmActionText="Delete"
      onConfirm={() => {
        console.log("Delete project")
      }}
      renderContent={() => (
        <VStack gap={12}>
          <Text color="supporting">
            Are you sure you want to delete{" "}
            <Text as="span" color="regular">
              Job?
            </Text>{" "}
            This action will remove all the analytics related to the project.
          </Text>
          <Text color="supporting">
            To keep the data but hide the project from other parts of the app -{" "}
            <ShyTextButton
              onClick={() => {
                console.log("Make project inactive")
                onClose()
              }}
              text="make it inactive."'
            />
          </Text>
        </VStack>
      )}
    />
  )}
/>

The first component is a simple wrapper for a boolean state for an open/closed state, so it doesn't do much and is more of a personal preference.

import { ReactNode } from "react"
import { useBoolean } from "lib/shared/hooks/useBoolean"
import { ClosableComponentProps } from "lib/shared/props"

interface RenderOpenerParams {
  onOpen: () => void
}

interface Props {
  renderOpener: (params: RenderOpenerParams) => ReactNode
  renderContent: (params: ClosableComponentProps) => ReactNode
}

export const Opener = ({ renderOpener, renderContent }: Props) => {
  const [isOpen, { set: onOpen, unset: onClose }] = useBoolean(false)

  return (
    <>
      {isOpen && renderContent({ onClose })}
      {renderOpener({ onOpen })}
    </>
  )
}

The second component utilizes the Modal component, which I've explained in a separate post. We base ConfirmationModalProps of ModalProps while making onClose a required prop and removing the footer, as we want to use it for two choices, cancel or confirm. To make both buttons equal in size, I leverage the SameWidthChildrenRow, which you can learn more about here.

import { Modal, ModalProps } from "."
import { SameWidthChildrenRow } from "../Layout/SameWidthChildrenRow"
import { PrimaryButton, PrimaryButtonKind } from "../buttons/rect/PrimaryButton"

interface ConfirmationModalProps
  extends Omit<ModalProps, "footer" | "onClose"> {
  onClose: () => void
  onConfirm: () => void
  confirmActionText: string
  confirmActionKind?: PrimaryButtonKind
}

export const ConfirmationModal = ({
  onClose,
  onConfirm,
  confirmActionText,
  confirmActionKind = "alert",
  ...props
}: ConfirmationModalProps) => {
  return (
    <Modal
      {...props}
      onClose={onClose}
      footer={
        <SameWidthChildrenRow gap={20}>
          <PrimaryButton size="l" onClick={onClose} kind="secondary">
            Cancel
          </PrimaryButton>
          <PrimaryButton
            size="l"
            onClick={() => {
              onConfirm()
              onClose()
            }}
            kind={confirmActionKind}
          >
            {confirmActionText}
          </PrimaryButton>
        </SameWidthChildrenRow>
      }
    />
  )
}