😶‍🌫️

React × Material-UIでユーザーの入力を待機する確認ダイアログカスタムフックの実装

2023/04/04に公開
1

React と Material-UI(以下MUI)を使った確認ダイアログの実装についてのメモです。

やりたいこと

以下のように処理の途中で確認ダイアログを呼び、ユーザーの入力に応じて処理できるようにするのが目標です。

const onClick = async () => {
  const result = await openDialog(); // ここでダイアログを開き、ユーザーの入力を待機
  if (result === 'confirm') {  // ユーザーがOKした場合
    console.log('Confirmed');
  } else {  // ユーザーがキャンセルした場合
    console.log('Canceled');
  }
};

1. 確認ダイアログの実装

1.1 確認ダイアログコンポーネントの作成

まず、MUIを利用してシンプルな確認ダイアログコンポーネントを作成しました。

useConfirmDialog.ts
import { Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, Button } from "@mui/material";
import { FC } from "react";

type ConfirmDialogResult = "confirm" | "cancel";

type ConfirmDialogProps = {
  open: boolean;
  onClose: (result: ConfirmDialogResult) => void;
  title: string;
  message: string;
};

const _ConfirmDialog: FC<ConfirmDialogProps> = ({ open, onClose, title, message }) => (
  <Dialog open={open} onClose={() => onClose("cancel")}>
    <DialogTitle>{title}</DialogTitle>
    <DialogContent>
      <DialogContentText>{message}</DialogContentText>
    </DialogContent>
    <DialogActions>
      <Button onClick={() => onClose("cancel")} variant="outlined">
        キャンセル
      </Button>
      <Button onClick={() => onClose("confirm")} autoFocus>
        確認
      </Button>
    </DialogActions>
  </Dialog>
);

ユーザーが確認した場合はonClose("confirm")、キャンセルした場合はonClose("cancel")を呼びます。

また、ダイアログのタイトルとメッセージをpropsで渡すことができます。

見た目はこんな感じです

1.2 カスタムフックの作成

ここが本題です。以下のように確認ダイアログコンポーネント(ConfirmDialog)と、ダイアログを開くための関数(openConfirmDialog)を返すカスタムフックを作成しました。

useConfirmDialog.ts
export const useConfirmDialog = () => {
  const [open, setOpen] = useState(false);
  const [resolve, setResolve] = useState<(value: ConfirmDialogResult) => void>();

  const openConfirmDialog = () => {
    setOpen(true);
    return new Promise<ConfirmDialogResult>((resolve) => {
      setResolve(() => resolve);
    });
  };

  const onClose = (result: ConfirmDialogResult) => {
    setOpen(false);
    if (resolve) {
      resolve(result);
    }
  };

  const ConfirmDialog: FC<{ title: string; message: string }> = (props) => (
    <_ConfirmDialog open={open} onClose={onClose} title={props.title} message={props.message} />
  );

  return {
    ConfirmDialog,
    openConfirmDialog,
  };
};

ポイントとしてはダイアログを開く際に呼ばれるopenConfirmDialogの中でPromiseを返し、ダイアログを閉じる際に呼ばれるonCloseの中でresolveを呼ぶことでユーザーの確認またはキャンセルの選択後にPromiseが解決されるようになっている点です。

これによってdialogが閉じられた際の結果を返すことができます。

  const openConfirmDialog = () => {
    setOpen(true);
    return new Promise<ConfirmDialogResult>((resolve) => { // Promiseを返す
      setResolve(() => resolve); // resolveをstateにset
    });
  };

  const onClose = (result: ConfirmDialogResult) => {
    setOpen(false);
    if (resolve) {
      resolve(result);  // 結果をresolveで返す
    }
  };

2. 使い方

App.tsx
import { FC } from "react";
import { Button } from '@material-ui/core';
import useConfirmDialog from './useConfirmDialog';

const App: FC = () => {
  const [ConfirmDialog, openConfirmDialog] = useConfirmDialog();

  const onClick = async () => {
    const result = await openConfirmDialog();
    if (result === "confirm") {
      console.log("Confirmed!!");
    } else {
      console.log("Canceled");
    }
  };

  return (
    <>
      <Button onClick={onClick}>確認ダイアログを開く</Button>
      <ConfirmDialog title="確認ダイアログ" message="本当に実行しますか?" />
    </>
  );
};

export default App;

openConfirmDialogでダイアログを開き、ユーザーの入力を待って、その結果に応じて処理を行なっています。やりたかったことが実現できました!

参考

https://zenn.dev/longbridge/articles/cf8c7171458732

Discussion