Oteto Blogのロゴ

【React】window.confirmの代替となるPromiseな確認ダイアログを自作する

ブラウザ標準ではなく任意のUIで確認ダイアログを実装したい場面があったので、今回はwindow.confirm()のようにメインスレッドをブロックするモーダルをReact Hooksを利用して実装してみる。

1. カスタムフックを実装

import { useCallback, useState } from 'react';

type Confirm = {
  isOpen: boolean;
  resolve: (() => void) | null;
  reject: (() => void) | null;
};

export default function useConfirm() {
  const [confirm, setConfirm] = useState<Confirm>({
    isOpen: false,
    resolve: null,
    reject: null,
  });

  const isConfirmed = useCallback(() => {
    const promise = new Promise<void>((resolve, reject) => {
      setConfirm({ isOpen: true, resolve, reject });
    });
    return promise.then(
      () => true,
      () => false,
    );
  }, []);

  const reset = useCallback(() => {
    setConfirm({ isOpen: false, resolve: null, reject: null });
  }, []);

  return { ...confirm, isConfirmed, reset };
}

2. モーダルを実装

import { type ReactElement, type ReactNode, useEffect, useRef } from 'react';

type Params = {
  children: ReactNode;
  onClose?: () => void;
};

export default function OpenedModal({
  children,
  onClose,
}: Params): ReactElement {
  const dialogRef = useRef<HTMLDialogElement | null>(null);

  useEffect(() => {
    if (!dialogRef.current?.open) dialogRef.current?.showModal();
  }, []);

  return (
    <dialog ref={dialogRef} onClose={onClose}>
      {children}
    </dialog>
  );
}

ClickEventなどをトリガーとして開くイベント駆動のものではなく、最初から開いているモーダルを実装する。

またEscキーを押下して閉じされることを考慮してonCloseを引数として受け付けるようにする。

3. ConfirmButtonを実装

'use client';

import { type ReactElement, useCallback } from 'react';
import useConfirm from '@/presentation/hooks/use-confirm';
import OpenedModal from '@/presentation/components/common/modal/opened-modal';

export default function ConfirmButton(): ReactElement {
  const { isOpen, resolve, reject, isConfirmed, reset } = useConfirm();

  const handleClick = useCallback(async () => {
    // sample process
    if (await isConfirmed()) alert('OK');
    else alert('Cancel');

    reset();
  }, [isConfirmed, reset]);

  return (
    <>
      <button type="button" onClick={handleClick}>
        Confirm
      </button>
      {isOpen && (
        <OpenedModal onClose={reset}>
          <button type="button" onClick={reject ?? undefined}>
            Cancel
          </button>
          <button type="button" onClick={resolve ?? undefined}>
            OK
          </button>
        </OpenedModal>
      )}
    </>
  );
}

先ほど実装したuseConfirmフックを利用し、モーダル内のCancelボタンをクリックするとreject()、OKボタンをクリックするとresolve()を実行する。

そうすることでモーダル内のボタンを押下を待ち、押下後にalert()が実行されるようになる。