PromiseとuseSyncExternalStoreで自作confirm/alertを作成する
PromiseとuseSyncExternalStoreで自作confirm/alertを作成しました。
UIコンポーネントはShadcnのAlertDialogを利用しています。
該当のコードはこちらです。
'use client';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { AlertDialogDescription } from '@radix-ui/react-alert-dialog';
import * as VisuallyHidden from '@radix-ui/react-visually-hidden';
import { useSyncExternalStore } from 'react';
type DialogState = {
open: boolean;
title?: string;
type?: 'alert' | 'confirm';
resolve?: (value?: unknown) => void;
};
const defaultDialogState: DialogState = { open: false };
let dialogState: DialogState = defaultDialogState;
let listeners: (() => void)[] = [];
const dialogStore = {
subscribe: (listener: () => void) => {
listeners = [...listeners, listener];
return () => {
listeners = listeners.filter(l => l !== listener);
};
},
getSnapshot: () => dialogState,
getServerSnapshot: () => defaultDialogState,
setDialogState: (_dialogState: DialogState) => {
dialogState = _dialogState;
emitChange();
},
};
const emitChange = () => {
for (const listener of listeners) {
listener();
}
};
export const xAlert = (title: string) => {
dialogStore.setDialogState({ open: true, title, type: 'alert' });
};
export const xConfirm = (title: string) => {
return new Promise(resolve => {
dialogStore.setDialogState({ open: true, title, type: 'confirm', resolve: resolve });
});
};
export const AlertOrConfirmDialog = () => {
const { open, title, resolve, type } = useSyncExternalStore(
dialogStore.subscribe,
dialogStore.getSnapshot,
dialogStore.getServerSnapshot
);
const handleOk = () => {
resolve?.(true);
dialogStore.setDialogState({ open: false, title, resolve, type });
setTimeout(() => {
dialogStore.setDialogState(defaultDialogState);
}, 300);
};
const handleCancel = () => {
resolve?.(false);
dialogStore.setDialogState({ open: false, title, resolve, type });
setTimeout(() => {
dialogStore.setDialogState(defaultDialogState);
}, 300);
};
return (
<AlertDialog open={open} onOpenChange={open => !open && handleCancel()}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{title}</AlertDialogTitle>
<VisuallyHidden.Root>
<AlertDialogDescription>確認内容を表示しています。</AlertDialogDescription>
</VisuallyHidden.Root>
</AlertDialogHeader>
<AlertDialogFooter>
{type === 'confirm' && (
<AlertDialogCancel onClick={handleCancel}>キャンセル</AlertDialogCancel>
)}
<AlertDialogAction onClick={handleOk} autoFocus>
{type === 'confirm' && 'OK'}
{type === 'alert' && '閉じる'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
AlertOrConfirmDialogコンポーネントはNext.jsの場合はlayout.tsx、vite利用でシンプルなReactの場合はApp.tsxでしょうか、アプリケーション全体で使用できる箇所に配置します。
Dialogの開閉状態や、内部のテキスト情報をコンポーネントの外部の変数dialogStateに格納し、
それをuseSyncExternalStoreで購読しています。
useSyncExternalStoreによる外部ストアの購読方法はこちらにサンプルがあります。 https://ja.react.dev/reference/react/useSyncExternalStore#subscribing-to-an-external-store
外部変数で状態を管理することで呼び出すときには
import { xAlert } from "@/components/AlertOrConfirmDialog";
try {
// 目的の処理
} catch (error) {
console.error(error);
xAlert("エラーが発生しました。");
}
のようにimportのみ行っておけば、カスタムフックスを挟んだりせず、簡易に呼び出せます。
useStateやそのほかグローバルな状態管理ライブラリに依存している場合は、
どうしても呼び出し時にカスタムフックスが必要になります。
またconfirmに関してはPromiseを用いて、
OKボタンを押下した場合に後続の処理を行うように実装しています。
export const xConfirm = (title: string) => {
return new Promise(resolve => {
dialogStore.setDialogState({ open: true, title, type: 'confirm', resolve: resolve });
});
};
const handleOk = () => {
resolve?.(true);
dialogStore.setDialogState({ open: false, title, resolve, type });
setTimeout(() => {
dialogStore.setDialogState(defaultDialogState);
}, 300);
};
const handleCancel = () => {
resolve?.(false);
dialogStore.setDialogState({ open: false, title, resolve, type });
setTimeout(() => {
dialogStore.setDialogState(defaultDialogState);
}, 300);
};
Dialog内のOKボタンやキャンセルボタン押下時に、Promiseが解決するようにし、戻り値のbooleanをもとに後続の処理を行うか判定しています。
呼び出しは簡単で下記にように使用します。
import { xConfirm } from "@/components/AlertOrConfirmDialog";
const onClick = async () => {
if (await xConfirm("処理を行いますか?")) {
// 後続の処理
}
};
以上が、自作confirm/alertの作成方法となります。
上記コードでは、Dialogを閉じる時のUIのがたつき防止のためにsetTimeoutを使用していたり、
ShadcnのAlertDialogではDescriptionに該当するものがない時にアクセシビリティのエラーが出るため、その対応のためのコードも入っています。
この辺りはモーダルコンポーネントの作成に利用するUIコンポーネントの事情によるところでもあり、微調整が必要になるところかと思います。