MUI5, React18を使って確認ダイアログを作る

2023.09.28

ベルリンオフィスの畑ですこんにちは。

先日ヘッドレスCMSのContentfulのロール管理アプリをNext.js+MUI5で作っていたのですが、何らかのアクションを起こした際に確認ダイアログを表示したいとなりました。本記事では備忘もかねてその方法を書きたいと思います。

こんなやつです。具体的な要件は

  • MUI5で作る
  • Reactのバージョンは18
  • ボタンクリックなどのイベントに応じて確認ダイアログを表示する
  • ボタンに限らずあらゆる場面で使用可能なものがよい(関数内での使用やn秒後の自動表示なども可能)

検索してみるとカスタムフックの作成など方法はいくつかあるようです。今回私が実装した方法では基本的に関数のインポート文・呼び出し文を書くだけで使えるという点が便利かと思います。

確認ダイアログコンポーネント

confirm-dialog.tsx

import { useState } from "react";
import { Root, createRoot } from "react-dom/client";
import { Button, Dialog, DialogActions, DialogContent, DialogContentText } from "@mui/material";

export const showConfirmDialog = async (message: string, cancel = false): Promise<boolean> => {
  const rootEl = document.getElementById("root"); // "root"はルート要素のIDに応じて変更ください
  const newEl = document.createElement("div");
  newEl.id = "confirm";
  rootEl?.appendChild(newEl);
  const container = document.getElementById("confirm");
  if (!container) return false;
  const root = createRoot(container);

  return new Promise<boolean>((resolve, reject) => {
    try {
      root.render(<ConfirmDialog root={root} message={message} resolve={resolve} cancel={cancel} />);
    } catch (err) {
      reject(err);
    }
  });
};

type Props = {
  root: Root; //ルート要素
  message: string; //メッセージ内容
  resolve: (f: boolean) => void; // 確認ダイアログが閉じられたときに true または false を渡すためのコールバック関数
  cancel: boolean; // キャンセルボタン表示・非表示を制御
};

function ConfirmDialog({ root, message, resolve, cancel = false }: Props) {
  const [open, setOpen] = useState(true);

  const handleClose = () => {
    setOpen(false);
    root.unmount();
    resolve(false);
  };

  const clickOk = () => {
    setOpen(false);
    root.unmount();
    resolve(true);
  };

  return (
    <>
      <Dialog open={open} onClose={handleClose}>
        <DialogContent>
          <DialogContentText>{message}</DialogContentText>
        </DialogContent>
        <DialogActions>
          {cancel && (
            <Button autoFocus onClick={handleClose}>
              Cancel
            </Button>
          )}
          <Button onClick={clickOk}>OK</Button>
        </DialogActions>
      </Dialog>
    </>
  );
}

confirm-dialog.tsxは確認ダイアログのコンポーネントです。 showConfirmDialog 関数が確認ダイアログを表示するためのエントリーポイントとなります。ダイアログを表示したい場所からこの関数を呼べばOKです。 関数内で新しい要素を作成し、それをページに追加してカスタムダイアログを表示し、ユーザーの操作を待ちます。ユーザーがダイアログを閉じるとPromiseが解決され、ユーザーが "OK" を選択したかどうかが true または false で返されます。

注意点としては呼び出し画面においてルート要素の指定が必要となります。React 18からReactDOM.renderが非推奨となり、代わりのcreateRootではroot要素の指定が必須となったためです。その指定は次のcontent.tsxで行います。

ダイアログを呼ぶ側

content.tsx

import { showConfirmDialog } from "@/components/confirm-dialog";
・・・中略・・・
   return (
    <>
      <Head>
        <title>Edit Role Content</title>
        <meta name="description" content="Generated by create next app" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <main id="root">
       <Grid container>
・・・中略・・・
        <Button onClick={() => handleDeleteRole(row.sys.id, idx)}>Delete</Button>
・・・中略・・・
  const handleDeleteRole = async (id: string, idx: number) => {
    const confirmed = await showConfirmDialog("ロールを削除しますか?", true);
    if (!confirmed) return;
    await deleteRole(id);
  };

content.tsxは確認ダイアログを表示したい画面です。前述のルートとなる要素にid="root"を付与しています(11行目。これがないとルート要素を取得できないのでダイアログ表示されません))。 全画面にこれを記載するのは手間なので共通レイアウトに入れるのがよいと思います。

あとはshowConfirmDialogをimportし、イベントハンドラなどから呼び出すだけです。ここでは「Delete」ボタンをクリックするとhandleDeleteRoleが呼ばれ、この関数内でshowConfirmDialogを実行しています。ここではOKボタンが押され戻り値(confirmed)=trueとなる場合のみ、後続の処理つまりロール削除を実行しています。 (ロールは単純にユーザー関連のデータと考えてください)

以上、参考になりましたら幸いです😺