Skip to main content

useModal

useModal provides a global, promise-based modal API and a controller for building controlled modals. It is backed by a ModalProvider that renders two Bootstrap modals: a regular modal and a confirm modal. Source: src/states/useModalContext.tsx

Provider

Wrap your app with ModalProvider. The default LocationContext already includes it, so you usually don’t need to add it yourself.
// src/states/LocationContext.tsx
<LocationByRouteProvider>
  <AuthProvider>
    <LocationSettingsProvider>
      <CSSLoaderComponent url="/api/styles/styles.css">
        <ModalProvider>{children}</ModalProvider>
      </CSSLoaderComponent>
    </LocationSettingsProvider>
  </AuthProvider>
</LocationByRouteProvider>

API

type ModalContent = ReactNode | { render: (controller: ModalController) => ReactNode }

type ModalOptions = Partial<{
  title: string
  confirmText: string
  cancelText: string
  size: 'sm' | 'lg' | 'xl'
  bodyClassName: string
  confirmButtonClass: string // e.g. 'primary', 'danger'
  confirmHandler: () => () => Promise<void>
}>

type ModalContext = {
  showModal: (content: ModalContent, options?: ModalOptions) => Promise<void>
  hideModal: () => void
  confirm: (message: ModalContent, options?: ModalOptions) => Promise<boolean>
  hideAllModals: () => void
  setModalsVisible: (visible: boolean) => void
}

type ModalController = {
  setTitle(text: string): void
  setConfirmButtonText(text: string): void
  setCancelButtonText(text: string): void
  setConfirmButtonLoading(loading: boolean): void
  setCancelButtonLoading(loading: boolean): void
  setConfirmButtonDisabled(disabled: boolean): void
  setCancelButtonDisabled(disabled: boolean): void
  setOnConfirm(fn: () => Promise<void>): void
  close(): void
}

Quick use

import { useModal } from '@/states/useModalContext'

const Example = () => {
  const { showModal, confirm } = useModal()

  const openInfo = async () => {
    await showModal(<div>Hello world</div>, { title: 'Info' })
  }

  const ask = async () => {
    const ok = await confirm('Delete this item?', { title: 'Confirm', confirmText: 'Delete', confirmButtonClass: 'danger' })
    if (ok) {
      // proceed
    }
  }

  return (
    <>
      <button onClick={openInfo}>Open modal</button>
      <button onClick={ask}>Confirm</button>
    </>
  )
}

Controlled modals (advanced)

Controlled modals let the modal content drive button text, loading state, and confirm behavior using the ModalController. To opt in, pass a content object with a render(controller) function. Inside, call controller setters to update button state or register an async confirm handler.
const { showModal } = useModal()

const openControlled = async () => {
  await showModal(
    {
      render: (controller) => (
        <form
          onSubmit={(e) => {
            e.preventDefault()
            controller.setOnConfirm(async () => {
              controller.setConfirmButtonLoading(true)
              try {
                // await API call
              } finally {
                controller.setConfirmButtonLoading(false)
              }
            })
          }}>
          <input placeholder="Name" />
        </form>
      ),
    },
    { title: 'Create', confirmText: 'Save' },
  )
}
How it works:
  • When you call showModal, the provider renders the regular modal.
  • If your content is { render(controller) }, the provider calls your render function and injects a ModalController.
  • You then call controller.setOnConfirm(fn) to register the confirm handler; when the user clicks the confirm button (or closes), the provider executes it with error handling:
    • Shows a LoadingSpinner while the promise is pending (setConfirmButtonLoading(true))
    • Displays an error Alert inside the modal if your handler throws
    • Closes the modal and resolves the promise when the handler completes successfully
Notes:
  • You can dynamically change title and button labels using setTitle, setConfirmButtonText, and setCancelButtonText.
  • Disable or enable buttons via setConfirmButtonDisabled and setCancelButtonDisabled.
  • Call controller.close() to programmatically close the active modal.

Confirm modals with custom content

confirm(content, options) behaves similarly, but displays a Cancel + Confirm footer. You can also control it via the same controller pattern:
const ok = await confirm(
  {
    render: (controller) => (
      <div>
        Are you absolutely sure?
        {/* Example: attach extra async work to the confirm action */}
        {controller.setOnConfirm(async () => {
          /* async side-effects */
        })}
      </div>
    ),
  },
  { title: 'Danger zone', confirmText: 'Yes, do it', confirmButtonClass: 'danger' },
)

Error handling and loading

  • The provider wraps your confirm handler in a try/catch and surfaces errors via a danger Alert in the modal body.
  • LoadingSpinner is shown automatically on the confirm button while your handler is pending.

Accessibility and visibility

  • setModalsVisible(false) hides the modal UI but keeps state. Useful for embedded flows where the host temporarily forbids overlays.
  • Modals are React-Bootstrap Modal components; ensure surrounding UI remains keyboard navigable.

See also