zustandで便利な確認ダイアログを作る
開発環境
- next 14.2.15
- zustand 5.0.0
- @headlessui/react 2.2.0
前提
状態管理はzustand、ダイアログはheadlessUIを利用していますが、必須ではありません。
他のライブラリでも代用は可能です。
本題
よくあるデータ削除前の確認用の「本当に削除しますか?」という、あのダイアログを実装してみます。
要件としては以下です。
- どこからでも簡単に利用できること
- formのonSubmit関数のような送信処理の中で利用すること
1. 確認ダイアログのデザインと構造
まず、確認ダイアログのUIを構築します。以下のConfirmDialogコンポーネントでは、@headlessui/reactとlucide-reactを利用し、ダイアログの開閉やボタンの設定を行います。
import { Fragment } from 'react';
import { Dialog, Transition, TransitionChild, DialogPanel } from '@headlessui/react';
import { XIcon } from 'lucide-react';
import { Button } from './button';
type ConfirmDialogProps = {
title: string;
message: string;
close: (arg: boolean) => void;
};
export const ConfirmDialog = ({ title, message, close }: ConfirmDialogProps) => {
return (
<Transition appear show as={Fragment}>
<Dialog as="div" className="relative z-40" onClose={() => null}>
<TransitionChild as={Fragment} enter="ease-out duration-300" enterFrom="opacity-0" enterTo="opacity-100" leave="ease-in duration-200" leaveFrom="opacity-100" leaveTo="opacity-0">
<div className="bg-black/70 fixed inset-0" />
</TransitionChild>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center text-center">
<TransitionChild as={Fragment} enter="ease-out duration-300" enterFrom="opacity-0 scale-95" enterTo="opacity-100 scale-100" leave="ease-in duration-200" leaveFrom="opacity-100 scale-100" leaveTo="opacity-0 scale-95">
<DialogPanel className="shadow-300 bg-white relative w-auto min-w-80 max-w-xs overflow-hidden rounded-2xl border px-5 py-7 text-left align-middle transition-all sm:min-w-96 sm:max-w-md">
<div className="absolute right-1 top-1">
<Button size="icon" variant="ghost" onClick={() => close(false)}>
<XIcon />
</Button>
</div>
<div className="my-2 font-bold">{title}</div>
<div className="mt-2">
<p className="whitespace-pre-wrap [overflow-wrap:anywhere]">
{message}
</p>
</div>
<div className="mt-5 flex justify-end gap-2">
<Button variant="secondary" onClick={() => close(false)}>いいえ</Button>
<Button onClick={() => close(true)}>はい</Button>
</div>
</DialogPanel>
</TransitionChild>
</div>
</div>
</Dialog>
</Transition>
);
};
2. 状態管理(zustand)によるダイアログ制御
続いて、zustandで確認ダイアログの開閉を管理します。以下のConfirmStoreで、開閉の状態や確認結果をPromiseで返すように設定しています。
import { create } from 'zustand';
export type ConfirmStore = {
confirm?: {
title: string;
message: string;
resolve: (arg: boolean) => void;
};
open: (confirm: { title: string; message: string }) => Promise<boolean>;
close: (result: boolean) => void;
};
export const useConfirmStore = create<ConfirmStore>((set) => ({
open: (confirm) =>
new Promise((resolve) => {
set((state) => ({
...state,
confirm: {
...confirm,
resolve,
},
}));
}),
close: (arg) => {
set((state) => {
state.confirm?.resolve(arg);
return { ...state, confirm: undefined };
});
},
}));
ここでは、openでダイアログのメッセージやタイトルを設定し、Promiseで確認の結果を受け取れるようにしています。closeでは確認結果を返してダイアログを閉じることができます。
3. プロバイダーによるグローバル適用
ConfirmProviderを作成して、ダイアログコンポーネントがどの画面からでも使用できるように設定します。これにより、confirm状態が変わるとダイアログが自動的に表示されます。
'use client';
import type { ReactNode } from 'react';
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import { useConfirmStore } from '@/stores/confirm';
export const ConfirmProvider = ({ children }: { children: ReactNode }) => {
const { confirm, close } = useConfirmStore();
return (
<>
{children}
{confirm && <ConfirmDialog {...confirm} close={close} />}
</>
);
};
confirmが存在する場合にConfirmDialogがレンダリングされるため、設定した状態に応じてダイアログが表示されます。
このプロバイダーはsrc/app/layout.tsxなどのファイルで利用することを想定しています。
4. 実際の使用例
最後に、確認ダイアログの利用例です。ここではボタンをクリックした際に確認ダイアログを表示し、ユーザーが「はい」をクリックするとconsole.logで処理が実行されます。
'use client';
import { Button } from '@/components/ui/button';
import { useConfirmStore } from '@/stores/confirm';
export const ConfirmButton = () => {
const { open } = useConfirmStore();
return (
<Button
onClick={async () => {
if (await open({ title: '確認', message: '本当に削除しますか?' })) {
console.log('---submit---');
}
}}
>
削除
</Button>
);
};
この例では、ボタンをクリックするとopenメソッドが呼ばれ、ダイアログが表示されます。「はい」または「いいえ」を選択することで、結果がPromiseで返され、後続処理に進むことができます。
「はい」をクリックしたときだけ、コンソールログが出力されるはずです。
さいごに
今回紹介した方法で、確認ダイアログを簡単に実装し、どのコンポーネントからでも柔軟に使用できるようになります。
最後の使用例では分かりやすくするために単純なボタンコンポーネントをクリックした挙動を紹介しましたが、formのonSubmit関数のような送信処理の中で利用することももちろん可能です。