カスタム確認ダイアログをReact hooksで実装する
これは CastingONE Advent Calendar 2023 4日目の記事です。
始めに
確認ダイアログを使う際に、window.confirm
だと以下のようにreturnでtrue/falseで返ってくるため処理の流れが読みやすいです。
const Page: FC = () => {
return (
<Button
variant="contained"
onClick={() => {
const result = window.confirm("本当によろしいですか?");
if (result === true) {
enqueueSnackbar("実行しました", {
variant: "success"
});
}
}}
>
確認
</Button>
)
}
しかし自作のダイアログを使った場合、よくisOpen
のような開閉フラグを用意して表示することが多く、処理の流れが一度途切れてしまいます。
const Page: FC = () => {
const [isOpen, setIsOpen] = useState(false)
return (
<Box>
<Button
variant="contained"
onClick={() => {
setIsOpen(true);
// ここで処理が途切れてしまう
}}
>
確認
</Button>
<Dialog
open={isOpen}
onClose={() => {
setIsOpen(false);
}}
>
<DialogTitle>確認ダイアログ</DialogTitle>
<DialogContent>本当によろしいですか?</DialogContent>
<DialogActions>
<Button
variant="outlined"
onClick={() => {
setIsOpen(false);
}}
>
キャンセル
</Button>
<Button
variant="contained"
onClick={() => {
// 確認がOKだった時の処理が以下になるが、UIのコードも混ざっていて処理の流れが掴みづらい
enqueueSnackbar("実行しました", {
variant: "success"
});
setIsOpen(false);
}}
>
OK
</Button>
</DialogActions>
</Dialog>
</Box>
)
}
これが以下のような感じで確認メソッドを実行したら自動でダイアログが開き、その結果がコールバックとして受け取れるようになったら処理の流れが分かりやすく、スッキリしたコードになるのでは思い、それを作ることができるhooksを作ってみましたので備忘録としてまとめました。
// 呼ぶだけで目的のカスタムダイアログを表示できるhooks
const useSimpleCustomConfirm = () => {
// カスタムUIの内容を定義する汎用的なhooksを呼んで、UI部分だけ定義する
return useCustomConfirm({
renderContent: ({ ok, cancel }) => {
return (
<>
<DialogTitle>シンプル確認ダイアログ</DialogTitle>
<DialogContent>本当によろしいですか?</DialogContent>
<DialogActions>
<Button variant="outlined" onClick={cancel}>
キャンセル
</Button>
<Button variant="contained" onClick={ok}>
OK
</Button>
</DialogActions>
</>
)
}
});
}
const Page: FC = () => {
// 確認ダイアログを出すメソッドとダイアログ表示用のReact要素を返す
const { confirm, dialogElement } = useSimpleCustomConfirm();
return (
<Box>
<Button
variant="contained"
onClick={() => {
// 確認メソッドを実行するとダイアログが表示されて、OKであった場合の処理を引数にかける
confirm({
onOk: () => {
enqueueSnackbar("実行しました", {
variant: "success",
});
},
});
}}
>
確認
</Button>
{dialogElement}
</Box>
);
};
カスタム確認ダイアログをReact hooksで実装する方法
上の例で見せたようなものを実装していきますが、更に確認ダイアログを開く際に追加のパラメータを渡せたり、非同期実行時にローディングを出すなど様々なユースケースにも対応できるように拡張していきたいと思います。
シンプルなカスタムUIに対応できるhooksを作る
まずは上の例で見せたコンテンツ部分も自由に定義できるuseCustomConfirm
を作ります。以下のようなコードを書くことで上の例で見せたシンプルなカスタム確認ダイアログを作ることができます。
import { ReactElement, useState, useCallback } from "react";
import { Dialog } from "@mui/material";
export type UseCustomConfirmOption = {
/** コンテンツを描画する */
renderContent: (props: {
/** OKアクションを実行する */
ok: () => void;
/** キャンセルする */
cancel: () => void;
}) => ReactElement;
};
/** クリック時に実行されるハンドラー */
type ActionHandler = () => void;
export type ReturnUseCustomConfirm = {
/** 確認ダイアログを出す */
confirm: (props: { onOk: ActionHandler }) => void;
/** ダイアログ要素 */
dialogElement: ReactElement;
};
/**
* カスタマイズされた確認ダイアログを呼び出すhooks
*/
export const useCustomConfirm = function ({
renderContent,
}: UseCustomConfirmOption): ReturnUseCustomConfirm {
// ダイアログコンテンツ内でOKボタンを押したときに実行するメソッドをstateで持つ
const [execOk, setExecOk] = useState<ActionHandler | null>(null);
const handleOk = useCallback(() => {
if (execOk == null) {
return;
}
execOk();
setExecOk(null);
}, [execOk]);
const handleClose = useCallback(() => {
setExecOk(null);
}, []);
const confirm: ReturnUseCustomConfirm["confirm"] = useCallback(
({ onOk }) => {
setExecOk(() => onOk);
},
[]
);
// ダイアログ内に表示するReactElementを生成する
const dialogContentElement = renderContent({
ok: handleOk,
cancel: handleClose,
});
// OK時の実行メソッドを持っているかでダイアログを開くか判断する
const isOpen = execOk != null;
const dialogElement: ReturnUseCustomConfirm["dialogElement"] = (
<Dialog open={isOpen} onClose={handleClose}>
{dialogContentElement}
</Dialog>
);
return {
confirm,
dialogElement,
};
};
追加のパラメータを渡すことができるようにする
上のコードだけでも色々カスタマイズできると思いますが、開く際にパラメータを渡せるようになると更に自由度が上がると思うので、それができるようにします。
import { ReactElement, useState, useCallback } from "react";
import { Dialog } from "@mui/material";
+/** ダイアログコンテンツで追加する型 */
+type AdditionalContentProps<ContentProps = undefined> = [ContentProps] extends [
+ undefined
+]
+ ? {}
+ : {
+ props: ContentProps;
+ };
-export type UseCustomConfirmOption = {
+export type UseCustomConfirmOption<ContentProps = undefined> = {
/** コンテンツを描画する */
renderContent: (props: {
+ /** コンテンツに渡すprops */
+ props?: ContentProps;
/** OKアクションを実行する */
ok: () => void;
/** キャンセルする */
cancel: () => void;
}) => ReactElement;
};
/** クリック時に実行されるハンドラー */
type ActionHandler = () => void;
-export type ReturnUseCustomConfirm = {
+export type ReturnUseCustomConfirm<ContentProps = undefined> = {
/** 確認ダイアログを出す */
confirm: (
props: {
onOk: ActionHandler
+ } & AdditionalContentProps<ContentProps>
) => void;
/** ダイアログ要素 */
dialogElement: ReactElement;
};
/**
* カスタマイズされた確認ダイアログを呼び出すhooks
*/
-export const useCustomConfirm = function ({
+export const useCustomConfirm = function <ContentProps = undefined>({
renderContent,
-}: UseCustomConfirmOption): ReturnUseCustomConfirm {
+}: UseCustomConfirmOption<ContentProps>): ReturnUseCustomConfirm<ContentProps> {
// ダイアログコンテンツ内でOKボタンを押したときに実行するメソッドをstateで持つ
const [execOk, setExecOk] = useState<ActionHandler | null>(null);
+ // 追加で渡すpropsをstateで持つ
+ const [contentProps, setContentProps] = useState<ContentProps>();
const handleOk = useCallback(() => {
if (execOk == null) {
return;
}
execOk();
setExecOk(null);
}, [execOk]);
const handleClose = useCallback(() => {
setExecOk(null);
}, []);
- const confirm: ReturnUseCustomConfirm["confirm"] = useCallback(
- ({ onOk }) => {
+ const confirm: ReturnUseCustomConfirm<ContentProps>["confirm"] = useCallback(
+ ({ onOk, ...restProps }) => {
+ if ("props" in restProps) {
+ setContentProps(restProps.props as ContentProps);
+ }
setExecOk(() => onOk);
},
[]
);
// ダイアログ内に表示するReactElementを生成する
const dialogContentElement = renderContent({
+ props: contentProps,
ok: handleOk,
cancel: handleClose,
});
// OK時の実行メソッドを持っているかでダイアログを開くか判断する
const isOpen = execOk != null;
const dialogElement: ReturnUseCustomConfirm["dialogElement"] = (
<Dialog open={isOpen} onClose={handleClose}>
{dialogContentElement}
</Dialog>
);
return {
confirm,
dialogElement,
};
};
こんな感じで呼び出すことで、動的に変更して使えるようになりました。開く前からパラメータを渡したいケースはラップしたhooksの引数側で受け取って使用すればよく、その辺も自由に決められます。
const useVariableSimpleCustomConfirm = ({ title }: { title: string }) => {
return useCustomConfirm<{ content: string }>({
renderContent: ({ props, ok, cancel }) => {
if (props == null) {
return <></>;
}
return (
<>
<DialogTitle>{title}</DialogTitle>
<DialogContent>{props.content}</DialogContent>
<DialogActions>
<Button variant="outlined" onClick={cancel}>
キャンセル
</Button>
<Button variant="contained" onClick={ok}>
OK
</Button>
</DialogActions>
</>
);
},
});
};
const Page: FC = () => {
const [contentText, setContentText] = useState("テキスト");
const { confirm, dialogElement } = useVariableCustomConfirm({
// hooks側でパラメータを渡すことも可能
title: "パラメータ変更可能確認ダイアログ",
});
return (
<Box>
<TextField
value={contentText}
label="コンテンツ"
size="small"
onChange={(event) => {
setContentText(event.target.value);
}}
/>
<br />
<Button
variant="contained"
onClick={() => {
confirm({
// confirm実行時に追加のプロパティを渡す
props: {
content: contentText,
},
onOk: () => {
enqueueSnackbar("実行しました", {
variant: "success",
});
},
});
}}
>
確認
</Button>
{dialogElement}
</Box>
);
};
非同期実行時にローディングを出せるようにする
続いてonOk時の処理が非同期だった場合の対応もしたいと思います。実際のケースでは確認ダイアログでOKだった場合にAPIリクエストを投げて、成功したらダイアログを閉じるケースがあると思いますが、現状だと非同期処理が考慮されていないためすぐ閉じてしまいます。非同期処理中はローディング表示にして、成功した時にダイアログを閉じる作りにしておくと毎回ローディングの設定も書く必要がなくなって便利そうです。
import { ReactElement, useState, useCallback } from "react";
import { Dialog } from "@mui/material";
/** ダイアログコンテンツで追加する型 */
type AdditionalContentProps<ContentProps = undefined> = [ContentProps] extends [
undefined
]
? {}
: {
props: ContentProps;
};
export type UseCustomConfirmOption<ContentProps = undefined> = {
/** コンテンツを描画する */
renderContent: (props: {
/** コンテンツに渡すprops */
props?: ContentProps;
+ /** 処理中か */
+ processing: boolean
/** OKアクションを実行する */
ok: () => void;
/** キャンセルする */
cancel: () => void;
}) => ReactElement;
};
/** クリック時に実行されるハンドラー */
-type ActionHandler = () => void;
+type ActionHandler = () => void | Promise<void>;
export type ReturnUseCustomConfirm<ContentProps = undefined> = {
/** 確認ダイアログを出す */
confirm: (
props: {
onOk: ActionHandler
} & AdditionalContentProps<ContentProps>
) => void;
/** ダイアログ要素 */
dialogElement: ReactElement;
};
/**
* カスタマイズされた確認ダイアログを呼び出すhooks
*/
export const useCustomConfirm = function <ContentProps = undefined>({
renderContent,
}: UseCustomConfirmOption<ContentProps>): ReturnUseCustomConfirm<ContentProps> {
+ // 非同期処理中かを管理するフラグ
+ const [isProcessing, setIsProcessing] = useState(false);
// ダイアログコンテンツ内でOKボタンを押したときに実行するメソッドをstateで持つ
const [execOk, setExecOk] = useState<ActionHandler | null>(null);
// 追加で渡すpropsをstateで持つ
const [contentProps, setContentProps] = useState<ContentProps>();
const handleOk = useCallback(() => {
if (execOk == null) {
return;
}
- execOk();
- setExecOk(null);
+ const result = execOk();
+ // OK時の処理がPromiseでないときは即閉じる
+ if (!(result instanceof Promise)) {
+ setExecOk(null);
+ return;
+ }
+ // OK時の処理がPromiseだった場合、処理中のフラグを立てて、resolveした時に閉じるようにする
+ setIsProcessing(true);
+ result
+ .then(() => {
+ setExecOk(null);
+ })
+ .catch((err) => {
+ console.log("catch reject", err);
+ })
+ .finally(() => {
+ setIsProcessing(false);
+ });
}, [execOk]);
const handleClose = useCallback(() => {
+ // 処理中の時は閉じれないようにする
+ if (isProcessing) {
+ return;
+ }
setExecOk(null);
- }, []);
+ }, [isProcessing]);
const confirm: ReturnUseCustomConfirm<ContentProps>["confirm"] = useCallback(
({ onOk, ...restProps }) => {
if ("props" in restProps) {
setContentProps(restProps.props as ContentProps);
}
setExecOk(() => onOk);
},
[]
);
// ダイアログ内に表示するReactElementを生成する
const dialogContentElement = renderContent({
props: contentProps,
+ processing: isProcessing,
ok: handleOk,
cancel: handleClose,
});
// OK時の実行メソッドを持っているかでダイアログを開くか判断する
const isOpen = execOk != null;
const dialogElement: ReturnUseCustomConfirm["dialogElement"] = (
<Dialog open={isOpen} onClose={handleClose}>
{dialogContentElement}
</Dialog>
);
return {
confirm,
dialogElement,
};
};
こんな感じで使用することを想定しています。折角なので非同期実行で成功/失敗を選択できたり、遅延待ち時間を設定できるようにしました。
type AsyncExecType = "success" | "fail";
const useAsyncCustomConfirm = () => {
return useCustomConfirm<{ asyncExecType: AsyncExecType; waitTime: number }>({
renderContent: ({ props, processing, ok, cancel }) => {
if (props == null) {
return <></>;
}
const asyncExecTypeLabel =
props.asyncExecType === "success" ? "成功" : "失敗";
return (
<>
<DialogTitle>非同期実行を待つダイアログ</DialogTitle>
<DialogContent>
<Box>非同期実行種別: {asyncExecTypeLabel}</Box>
<Box>遅延時間: {props.waitTime}ms</Box>
</DialogContent>
<DialogActions>
<LoadingButton
// 処理中はprocessingがtrueになるため、これでローディングの表示ができる
loading={processing}
variant="outlined"
onClick={cancel}
>
キャンセル
</LoadingButton>
<LoadingButton
loading={processing}
variant="contained"
onClick={ok}
>
OK
</LoadingButton>
</DialogActions>
</>
);
},
});
};
const Page: FC = () => {
const [waitTime, setWaitTime] = useState(1000);
const [asyncExecType, setAsyncExecType] = useState<AsyncExecType>("success");
const { confirm, dialogElement } = useAsyncCustomConfirm();
return (
<Box>
{/* waitTime, asyncExecTypeの設定部分は本質的な部分ではないので割愛 */}
<br />
<Button
variant="contained"
onClick={() => {
confirm({
props: {
asyncExecType,
waitTime,
},
onOk: () => {
// Promiseを返すことで解決するまでprocessingフラグが立って、ローディングが出る
// resolveした場合は自動で閉じて、rejectの場合はprocessingフラグだけOFFにしてその場に止まる
return new Promise((resolve, reject) => {
window.setTimeout(() => {
switch (asyncExecType) {
case "success":
resolve();
enqueueSnackbar("成功しました!", {
variant: "success",
});
return;
case "fail":
reject();
enqueueSnackbar("失敗しました。。", {
variant: "error",
});
return;
}
}, waitTime);
});
},
});
}}
>
確認
</Button>
{dialogElement}
</Box>
);
};
確認ダイアログ内でフォーム入力に対応する
最後に以下のような文字を入力して最終確認するような確認ダイアログをつけるようにすることを考えます。
これはこれ以上useCustomConfirm
を拡張する必要がなく、ラップしたhooksの方で調整するだけで実現できます。ローカルステートはコンポーネントにしないと持てないためそれ専用のフォームコンポーネントを作ってrenderContent
メソッドで呼び出すようにします。
const ConfirmForm: FC<{
confirmText: string;
processing: boolean;
onOk: () => void;
onCancel: () => void;
}> = ({ confirmText, processing, onOk, onCancel }) => {
const [inputText, setInputText] = useState("");
return (
<>
<DialogContent>
<Box>
確認のため「
<Typography component="span" fontWeight="bold">
{confirmText}
</Typography>
」と入力してください。
</Box>
<TextField
value={inputText}
fullWidth
size="small"
onChange={(event) => {
setInputText(event.target.value);
}}
/>
</DialogContent>
<DialogActions>
<LoadingButton
loading={processing}
variant="outlined"
onClick={onCancel}
>
キャンセル
</LoadingButton>
<LoadingButton
loading={processing}
variant="contained"
color="error"
disabled={inputText !== confirmText}
onClick={onOk}
>
OK
</LoadingButton>
</DialogActions>
</>
);
};
const useCustomConfirmWithForm = () => {
return useCustomConfirm<{ confirmText: string }>({
renderContent: ({ props, processing, ok, cancel }) => {
if (props == null) {
return <></>;
}
return (
<>
<DialogTitle>確認フォーム付きダイアログ</DialogTitle>
<ConfirmForm
confirmText={props.confirmText}
processing={processing}
onOk={ok}
onCancel={cancel}
/>
</>
);
}
});
};
const Page: FC = () => {
const { confirm, dialogElement } = useCustomConfirmWithForm();
return (
<Box>
<Button
variant="contained"
color="error"
onClick={() => {
confirm({
props: {
confirmText: "confirm"
},
onOk: () => {
return new Promise((resolve) => {
window.setTimeout(() => {
resolve();
enqueueSnackbar("実行しました", {
variant: "success"
});
}, 1000);
});
}
});
}}
>
確認
</Button>
{dialogElement}
</Box>
);
};
submit部分も拡張したuseCustomFormDialogを作る
今までは確認ダイアログのことだけにフォーカスを当てて作っていましたが、ボタン押下時にフォームダイアログ内で入力した内容も受け取れるようにしたら更に自由度を上げることができるかなと思い、useCustomFormDialog
も作ってみました。
import { ReactElement, useState, useCallback } from "react";
import { Dialog } from "@mui/material";
/** ダイアログコンテンツで追加する型 */
type AdditionalContentProps<ContentProps = undefined> = [ContentProps] extends [
undefined
]
? {}
: {
props: ContentProps;
};
+/** 送信メソッドの型 */
+type SubmitExecuter<SubmitData = undefined> = [SubmitData] extends [undefined]
+ ? () => void
+ : (submitData: SubmitData) => void;
export type UseCustomFormDialogOption<
ContentProps = undefined,
+ SubmitData = undefined
> = {
/** コンテンツを描画する */
renderContent: (props: {
/** コンテンツに渡すprops */
props?: ContentProps;
/** 処理中か */
processing: boolean;
- /** OKアクションを実行する */
- ok: () => void;
+ /** 送信する */
+ submit: SubmitExecuter<SubmitData>;
/** キャンセルする */
cancel: () => void;
}) => ReactElement;
};
-/** クリック時に実行されるハンドラー */
-type ActionHandler = () => void | Promise<void>;
+/** 送信時に実行されるハンドラー */
+type SubmitHandler<SubmitData = undefined> = (
+ data: SubmitData
+) => void | Promise<void>;
export type ReturnUseCustomFormDialog<
ContentProps = undefined,
+ SubmitData = undefined
> = {
- /** 確認ダイアログを出す */
- confirm: (
- props: {
- onOk: ActionHandler;
- } & AdditionalContentProps<ContentProps>
- ) => void;
+ /** フォームダイアログを表示する */
+ open: (
+ props: {
+ onSubmit: SubmitHandler<SubmitData>;
+ } & AdditionalContentProps<ContentProps>
+ ) => void;
/** フォームダイアログを閉じる */
close: () => void;
/** ダイアログ要素 */
dialogElement: ReactElement;
};
export const useCustomFormDialog = function <
ContentProps = undefined,
+ SubmitData = undefined
>({
renderContent
}: UseCustomFormDialogOption<
ContentProps,
+ SubmitData
>): ReturnUseCustomFormDialog<ContentProps, SubmitData> {
const [isProcessing, setIsProcessing] = useState(false);
- const [execOk, setExecOk] = useState<ActionHandler | null>(null);
+ const [execSubmit, setExecSubmit] = useState<SubmitHandler<
SubmitData
> | null>(null);
const [contentProps, setContentProps] = useState<ContentProps>();
const handleSubmit = useCallback(
+ (data: SubmitData) => {
if (execSubmit == null) {
return;
}
- const result = execOk();
+ const result = execSubmit(data);
if (!(result instanceof Promise)) {
setExecSubmit(null);
return;
}
setIsProcessing(true);
result
.then(() => {
setExecSubmit(null);
})
.catch((err) => {
console.log("catch reject", err);
})
.finally(() => {
setIsProcessing(false);
});
},
[execSubmit]
);
const handleClose = useCallback(() => {
// 処理中は閉じれないようにする
if (isProcessing) {
return;
}
setExecSubmit(null);
}, [isProcessing]);
const open: ReturnUseCustomFormDialog<
ContentProps,
SubmitData
>["open"] = useCallback(({ onSubmit, ...restProps }) => {
if ("props" in restProps) {
setContentProps(restProps.props as ContentProps);
}
setExecSubmit(() => onSubmit);
}, []);
const dialogContentElement = renderContent({
props: contentProps,
processing: isProcessing,
- ok: handleOk,
+ submit: handleSubmit as SubmitExecuter<SubmitData>,
cancel: handleClose
});
const isOpen = execSubmit != null;
const dialogElement: ReturnUseCustomFormDialog<
ContentProps,
+ SubmitData
>["dialogElement"] = (
<Dialog open={isOpen} onClose={handleClose}>
{dialogContentElement}
</Dialog>
);
return {
open,
close: handleClose,
dialogElement
};
};
これを使うと以下のようなことができます。例はオマケみたいなものなので最初は折り畳んだ状態で掲載しますので興味がある方は開いてみてください。
例: シンプルフォームダイアログ
const SimpleCustomForm: FC<{
onSubmit: (data: string) => void;
onCancel: () => void;
}> = ({ onSubmit, onCancel }) => {
const [text, setText] = useState("");
return (
<>
<DialogContent>
<TextField
sx={{ mt: 1 }}
value={text}
label="テキスト"
size="small"
fullWidth
onChange={(event) => {
setText(event.target.value);
}}
/>
</DialogContent>
<DialogActions>
<Button variant="outlined" onClick={onCancel}>
キャンセル
</Button>
<Button
variant="contained"
disabled={text === ""}
onClick={() => {
onSubmit(text);
}}
>
送信
</Button>
</DialogActions>
</>
);
};
const useSimpleCustomFormDialog = () => {
return useCustomFormDialog<undefined, string>({
renderContent: ({ submit, cancel }) => {
return (
<>
<DialogTitle>シンプルカスタムフォームダイアログ</DialogTitle>
<SimpleCustomForm onSubmit={submit} onCancel={cancel} />
</>
);
},
});
};
const Page: FC = () => {
const { open, dialogElement } = useSimpleCustomFormDialog();
return (
<Box>
<Button
variant="contained"
onClick={() => {
open({
onSubmit: (data) => {
enqueueSnackbar(`「${data}」を送信しました`, {
variant: "success"
});
}
});
}}
>
開く
</Button>
{dialogElement}
</Box>
)
}
例: 非同期設定ダイアログ
type AsyncExecType = "success" | "fail";
const AsyncSettingCustomForm: FC<{
processing: boolean;
onSubmit: (data: { asyncExecType: AsyncExecType; waitTime: number }) => void;
onCancel: () => void;
}> = ({ processing, onSubmit, onCancel }) => {
const [waitTime, setWaitTime] = useState(1000);
const [asyncExecType, setAsyncExecType] = useState<AsyncExecType>("success");
return (
<>
<DialogContent>
<FormControl>
<FormLabel>非同期実行種別</FormLabel>
<RadioGroup
value={asyncExecType}
row
onChange={(event) => {
setAsyncExecType(event.target.value as AsyncExecType);
}}
>
<FormControlLabel
value="success"
control={<Radio />}
label="成功"
/>
<FormControlLabel value="fail" control={<Radio />} label="失敗" />
</RadioGroup>
</FormControl>
<br />
<TextField
sx={{ mt: 1 }}
value={waitTime}
label="遅延時間"
type="number"
size="small"
onChange={(event) => {
const elInput = event.target;
if (!(elInput instanceof HTMLInputElement)) {
return;
}
setWaitTime(elInput.valueAsNumber);
}}
/>
</DialogContent>
<DialogActions>
<LoadingButton
loading={processing}
variant="outlined"
onClick={onCancel}
>
キャンセル
</LoadingButton>
<LoadingButton
loading={processing}
variant="contained"
onClick={() => {
onSubmit({
asyncExecType,
waitTime,
});
}}
>
送信
</LoadingButton>
</DialogActions>
</>
);
};
const useAsyncSettingCustomFormDialog = () => {
return useCustomFormDialog<
undefined,
{
asyncExecType: AsyncExecType;
waitTime: number;
}
>({
renderContent: ({ processing, submit, cancel }) => {
return (
<>
<DialogTitle>非同期設定ダイアログ</DialogTitle>
<AsyncSettingCustomForm
processing={processing}
onSubmit={submit}
onCancel={cancel}
/>
</>
);
},
});
};
const Page: FC = () => {
const { open, dialogElement } = useAsyncSettingCustomFormDialog();
return (
<Box>
<Button
variant="contained"
onClick={() => {
open({
onSubmit: (data) => {
return new Promise((resolve, reject) => {
window.setTimeout(() => {
switch (data.asyncExecType) {
case "success":
resolve();
enqueueSnackbar("成功しました", {
variant: "success",
});
return;
case "fail":
reject();
enqueueSnackbar("失敗しました。。", {
variant: "error",
});
return;
}
}, data.waitTime);
});
},
});
}}
>
開く
</Button>
{dialogElement}
</Box>
);
};
例: 確認フォームダイアログ
const ConfirmCustomForm: FC<{
confirmText: string;
processing: boolean;
onSubmit: () => void;
onCancel: () => void;
}> = ({ confirmText, processing, onSubmit, onCancel }) => {
const [inputText, setInputText] = useState("");
return (
<>
<DialogContent>
<Box>
確認のため「
<Typography component="span" fontWeight="bold">
{confirmText}
</Typography>
」と入力してください。
</Box>
<TextField
value={inputText}
fullWidth
size="small"
onChange={(event) => {
setInputText(event.target.value);
}}
/>
</DialogContent>
<DialogActions>
<LoadingButton
loading={processing}
variant="outlined"
onClick={onCancel}
>
キャンセル
</LoadingButton>
<LoadingButton
loading={processing}
variant="contained"
color="error"
disabled={inputText !== confirmText}
onClick={onSubmit}
>
送信
</LoadingButton>
</DialogActions>
</>
);
};
const useConfirmCustomFormDialog = () => {
return useCustomFormDialog<{ confirmText: string }>({
renderContent: ({ props, processing, submit, cancel }) => {
if (props == null) {
return <></>;
}
return (
<>
<DialogTitle>確認フォームダイアログ</DialogTitle>
<ConfirmCustomForm
confirmText={props.confirmText}
processing={processing}
onSubmit={submit}
onCancel={cancel}
/>
</>
);
}
});
};
const Page: FC = () => {
const { open, dialogElement } = useConfirmCustomFormDialog();
return (
<Box>
<Button
variant="contained"
color="error"
onClick={() => {
open({
props: {
confirmText: "confirm"
},
onSubmit: () => {
return new Promise((resolve) => {
window.setTimeout(() => {
resolve();
enqueueSnackbar("送信しました", {
variant: "success"
});
}, 1000);
});
}
});
}}
>
開く
</Button>
{dialogElement}
</Box>
)
}
検証コード
この記事で紹介したコードは以下のCodeSandboxにありますので、動きや詳細のコードを確認したい方はご覧ください。
終わりに
以上がカスタム確認ダイアログをReact hooksで実装する方法でした。カスタムUI部分もhooks側に閉じ込めて呼び出すだけでOK時の処理を設定できるようになったのでアプリケーション側が相当スッキリすると思いました。更に確認ダイアログが色々な画面で使われる場合は共通のものを使いやすくなったと思いました。
確認ダイアログだけではなく、更に拡張することでフォームダイアログについてもhooks側に切り出すことができ、相当汎用性が高くなったなと感じました。ただその分useCustomFormDialog
が複雑になりすぎて初見だと詳細の流れが理解しづらかったり、ボタンクリック時のロジックが長くなりすぎる可能性があるので状況に応じて使い分けていけると良いかなと思いました。
Discussion