React 削除確認ダイアログの設計パターン整理 - 5 パターンの完全実装比較
TL;DR
- 実装方式は 3 つの軸の組み合わせ: state の置き場(呼び出し側 / Provider / React tree 外)、呼び出し API(props / コールバック / Promise)、UI の提供形式(ライブラリ提供 / 利用側組み立て)
- 実在する組み合わせは大きく 5 パターン:
- プリミティブ + 呼び出し側 state(shadcn / Radix)
- Provider + コールバック方式(
showConfirm({onConfirm, onCancel})) - Provider + Promise 方式(
await confirm({...})- material-ui-confirm 系) - Provider なし命令的 API(react-confirm 系、
createRootで動的マウント) - Render Hook(hook が JSX を返す。日本ローカル)
- 「Provider 方式 = Promise ベース」とは限らない。コールバック方式は Promise の resolve トリックを使わずに済むため、resolve 漏れ・stale closure・concurrent rendering 等の罠を回避できる
このドキュメントの射程
- React で「アイテム削除時に確認ダイアログを出す」UI の実装方式を、実装可能な完全コードで比較
- 5 パターンそれぞれの長所・短所・トレードオフを示す
- 削除以外でも応用できる「確認ダイアログ」設計の決定基準
軸の整理
混同しがちな 3 つの軸を分離する:
| 軸 | 選択肢 |
|---|---|
| state の置き場 | (a) 呼び出し側の useState / (b) Provider の Context / (c) React tree 外(createRoot で動的マウント) |
| 呼び出し API | (α) props 駆動(open, onConfirm 等を渡す) / (β) コールバック方式(show({onConfirm})) / (γ) Promise 方式(await confirm()) |
| Dialog UI | (i) ライブラリが提供 / (ii) 利用側が組み立て(プリミティブ) |
「Promise-based」「Provider パターン」「Render Hook」はこの 3 軸の組み合わせの呼称。Provider 方式 ≠ Promise 方式 であることに注意。
共通の前提コード
以下、サンプル全体で使う型と削除関数:
// types.ts
export type Item = {
id: string;
name: string;
};
// api.ts
export async function deleteItem(id: string): Promise<void> {
const res = await fetch(`/api/items/${id}`, { method: 'DELETE' });
if (!res.ok) throw new Error(`Failed to delete item ${id}`);
}
UI ライブラリは shadcn/ui の AlertDialog を想定(Radix UI ベース、destructive style 対応)。
パターン A: プリミティブ + 呼び出し側 state(shadcn / Radix)
ライブラリは accessible なプリミティブを提供するだけで、state は呼び出し側で useState で管理する。
実装
// ItemList.tsx
import { useState } from 'react';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { Button } from '@/components/ui/button';
import { deleteItem } from './api';
import type { Item } from './types';
type Props = {
items: Item[];
onDeleted: (id: string) => void;
};
export function ItemList({ items, onDeleted }: Props) {
const [target, setTarget] = useState<Item | null>(null);
const [isDeleting, setIsDeleting] = useState(false);
const handleConfirm = async () => {
if (!target) return;
setIsDeleting(true);
try {
await deleteItem(target.id);
onDeleted(target.id);
setTarget(null);
} finally {
setIsDeleting(false);
}
};
return (
<>
<ul>
{items.map((item) => (
<li key={item.id}>
<span>{item.name}</span>
<Button variant="destructive" onClick={() => setTarget(item)}>
削除
</Button>
</li>
))}
</ul>
<AlertDialog
open={target !== null}
onOpenChange={(open) => {
if (!open && !isDeleting) setTarget(null);
}}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>削除確認</AlertDialogTitle>
<AlertDialogDescription>
「{target?.name}」を削除します。この操作は取り消せません。
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isDeleting}>キャンセル</AlertDialogCancel>
<AlertDialogAction onClick={handleConfirm} disabled={isDeleting}>
{isDeleting ? '削除中...' : '削除'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
}
特徴
- state: 呼び出し側
- API: props 駆動
- UI: 利用側組み立て
| 長所 | 短所 |
|---|---|
| 仕組みが透明、デバッグが容易 | 各 List ごとに useState と Dialog を書く必要 |
isDeleting などのローカル state と自然に連携できる | 削除以外の確認 UI で同じコードを書き直す |
| 「target は Item or null」で開閉と対象を 1 つの state にまとめられる | アプリ全体で統一したい場合は冗長 |
パターン B: Provider + コールバック方式
App root に <ConfirmProvider> を 1 つ置き、利用側は showConfirm({onConfirm, onCancel}) を呼ぶ。Promise の resolve トリックを使わず、コールバックで結果を受け取る。
実装
// ConfirmProvider.tsx
import {
createContext,
useCallback,
useContext,
useState,
type ReactNode,
} from 'react';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
type ShowConfirmOptions = {
title?: ReactNode;
description: ReactNode;
confirmText?: string;
cancelText?: string;
destructive?: boolean;
onConfirm: () => void | Promise<void>;
onCancel?: () => void;
};
type ConfirmContextValue = {
showConfirm: (options: ShowConfirmOptions) => void;
};
const ConfirmContext = createContext<ConfirmContextValue | null>(null);
export function ConfirmProvider({ children }: { children: ReactNode }) {
const [state, setState] = useState<ShowConfirmOptions | null>(null);
const [isPending, setIsPending] = useState(false);
const showConfirm = useCallback((options: ShowConfirmOptions) => {
setState(options);
}, []);
const handleConfirm = async () => {
if (!state) return;
setIsPending(true);
try {
await state.onConfirm();
setState(null);
} finally {
setIsPending(false);
}
};
const handleCancel = () => {
if (isPending) return;
state?.onCancel?.();
setState(null);
};
return (
<ConfirmContext.Provider value={{ showConfirm }}>
{children}
<AlertDialog
open={state !== null}
onOpenChange={(open) => {
if (!open) handleCancel();
}}
>
<AlertDialogContent>
<AlertDialogHeader>
{state?.title && <AlertDialogTitle>{state.title}</AlertDialogTitle>}
<AlertDialogDescription>{state?.description}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isPending}>
{state?.cancelText ?? 'キャンセル'}
</AlertDialogCancel>
<AlertDialogAction
onClick={handleConfirm}
disabled={isPending}
className={state?.destructive ? 'bg-destructive text-destructive-foreground hover:bg-destructive/90' : undefined}
>
{isPending ? '処理中...' : (state?.confirmText ?? 'OK')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</ConfirmContext.Provider>
);
}
export function useConfirm(): ConfirmContextValue {
const ctx = useContext(ConfirmContext);
if (!ctx) {
throw new Error('useConfirm must be used inside ConfirmProvider');
}
return ctx;
}
// App.tsx
import { ConfirmProvider } from './ConfirmProvider';
import { ItemListPage } from './ItemListPage';
export function App() {
return (
<ConfirmProvider>
<ItemListPage />
</ConfirmProvider>
);
}
// ItemList.tsx
import { useConfirm } from './ConfirmProvider';
import { deleteItem } from './api';
import type { Item } from './types';
type Props = {
items: Item[];
onDeleted: (id: string) => void;
};
export function ItemList({ items, onDeleted }: Props) {
const { showConfirm } = useConfirm();
const handleDelete = (item: Item) => {
showConfirm({
title: '削除確認',
description: (
<>
「<strong>{item.name}</strong>」を削除します。この操作は取り消せません。
</>
),
confirmText: '削除',
destructive: true,
onConfirm: async () => {
await deleteItem(item.id);
onDeleted(item.id);
},
});
};
return (
<ul>
{items.map((item) => (
<li key={item.id}>
<span>{item.name}</span>
<button onClick={() => handleDelete(item)}>削除</button>
</li>
))}
</ul>
);
}
特徴
- state: Provider が保持
- API: コールバック(
onConfirmを渡す) - UI: ライブラリ(Provider 内)が提供
| 長所 | 短所 |
|---|---|
| Promise の resolve トリック不要 → ref 不要、resolve 漏れなし | 呼び出し側コードが線形にならない(コールバックの中に処理を書く) |
onConfirm: async () => {} がそのまま使える | 「確認後に複数ステップ続く」場合にコールバックがネスト |
| アンマウント問題が起きにくい(hanging Promise が無い) | Promise を期待するライブラリ(React Suspense 等)と直接組み合わせにくい |
| concurrent rendering と相性が良い(state 駆動のみ) |
Promise との関係
onConfirm: async () => {} の戻り値 Promise を Provider が await する設計にしているので、削除中の loading 状態(isPending)を Provider 側で管理できる。「await すべき Promise」を作るのは呼び出し側、「await する」のは Provider なので、Promise が宙に浮く問題が起きない。
パターン C: Provider + Promise 方式(material-ui-confirm 系)
ライブラリ採用例が最も多い形。await confirm({...}) で線形に書ける。
実装
// ConfirmProvider.tsx
import {
createContext,
useCallback,
useContext,
useRef,
useState,
type ReactNode,
} from 'react';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
type ConfirmOptions = {
title?: ReactNode;
description: ReactNode;
confirmText?: string;
cancelText?: string;
destructive?: boolean;
};
type ConfirmState = ConfirmOptions & {
open: boolean;
};
const initialState: ConfirmState = {
open: false,
description: '',
};
type ConfirmFn = (options: ConfirmOptions) => Promise<boolean>;
const ConfirmContext = createContext<ConfirmFn | null>(null);
export function ConfirmProvider({ children }: { children: ReactNode }) {
const [state, setState] = useState<ConfirmState>(initialState);
const resolverRef = useRef<((confirmed: boolean) => void) | null>(null);
const confirm = useCallback<ConfirmFn>((options) => {
return new Promise<boolean>((resolve) => {
// 前回の Promise が残っている場合は false で resolve(hanging を防ぐ)
resolverRef.current?.(false);
resolverRef.current = resolve;
setState({ ...options, open: true });
});
}, []);
const close = (confirmed: boolean) => {
resolverRef.current?.(confirmed);
resolverRef.current = null;
setState((prev) => ({ ...prev, open: false }));
};
return (
<ConfirmContext.Provider value={confirm}>
{children}
<AlertDialog
open={state.open}
onOpenChange={(open) => {
if (!open) close(false);
}}
>
<AlertDialogContent>
<AlertDialogHeader>
{state.title && <AlertDialogTitle>{state.title}</AlertDialogTitle>}
<AlertDialogDescription>{state.description}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => close(false)}>
{state.cancelText ?? 'キャンセル'}
</AlertDialogCancel>
<AlertDialogAction
onClick={() => close(true)}
className={state.destructive ? 'bg-destructive text-destructive-foreground hover:bg-destructive/90' : undefined}
>
{state.confirmText ?? 'OK'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</ConfirmContext.Provider>
);
}
export function useConfirm(): ConfirmFn {
const ctx = useContext(ConfirmContext);
if (!ctx) {
throw new Error('useConfirm must be used inside ConfirmProvider');
}
return ctx;
}
// ItemList.tsx
import { useState } from 'react';
import { useConfirm } from './ConfirmProvider';
import { deleteItem } from './api';
import type { Item } from './types';
type Props = {
items: Item[];
onDeleted: (id: string) => void;
};
export function ItemList({ items, onDeleted }: Props) {
const confirm = useConfirm();
const [deletingId, setDeletingId] = useState<string | null>(null);
const handleDelete = async (item: Item) => {
const ok = await confirm({
title: '削除確認',
description: (
<>
「<strong>{item.name}</strong>」を削除します。この操作は取り消せません。
</>
),
confirmText: '削除',
destructive: true,
});
if (!ok) return;
setDeletingId(item.id);
try {
await deleteItem(item.id);
onDeleted(item.id);
} finally {
setDeletingId(null);
}
};
return (
<ul>
{items.map((item) => (
<li key={item.id}>
<span>{item.name}</span>
<button
onClick={() => handleDelete(item)}
disabled={deletingId === item.id}
>
{deletingId === item.id ? '削除中...' : '削除'}
</button>
</li>
))}
</ul>
);
}
特徴
- state: Provider が保持(+
useRefで resolver 関数を保持) - API: Promise(
await confirm()) - UI: ライブラリ(Provider 内)が提供
| 長所 | 短所 |
|---|---|
呼び出し側コードが線形(if (await confirm()) { ... }) | resolver を useRef で保持する命令的な仕掛けが必要 |
| 削除以外の確認 UI を統一できる | Promise が宙に浮く可能性(アンマウント、二重呼び出し) |
| 既存ライブラリ(material-ui-confirm 等)の API と一致 | await 跨ぎで item を握り続ける stale closure 問題 |
削除中の loading state は呼び出し側で別途持つ必要(パターン B と違い Provider が await していないため) |
material-ui-confirm v4 では「アンマウント時に Promise が永遠に pending になる」問題を { confirmed, reason } の API に変えることで解決している(reason: "unmount" を返す)。自前実装する場合も同様の配慮が必要。
パターン D: Provider なし命令的 API(react-confirm 系)
Provider を一切置かず、グローバル関数として import { confirm } from './confirm' のように使う。内部で React 18 の createRoot API を使って Dialog を document.body 直下に動的マウントする。
実装(react-confirm を使う場合)
// MyConfirmDialog.tsx
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import type { ConfirmDialogProps } from 'react-confirm';
import type { ReactNode } from 'react';
type Props = {
title?: ReactNode;
description: ReactNode;
confirmText?: string;
cancelText?: string;
destructive?: boolean;
};
export const MyConfirmDialog = ({
show,
proceed,
title,
description,
confirmText = 'OK',
cancelText = 'キャンセル',
destructive,
}: ConfirmDialogProps<Props, boolean>) => (
<AlertDialog
open={show}
onOpenChange={(open) => {
if (!open) proceed(false);
}}
>
<AlertDialogContent>
<AlertDialogHeader>
{title && <AlertDialogTitle>{title}</AlertDialogTitle>}
<AlertDialogDescription>{description}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => proceed(false)}>
{cancelText}
</AlertDialogCancel>
<AlertDialogAction
onClick={() => proceed(true)}
className={destructive ? 'bg-destructive text-destructive-foreground hover:bg-destructive/90' : undefined}
>
{confirmText}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
// confirm.ts
import { confirmable, createConfirmation } from 'react-confirm';
import { MyConfirmDialog } from './MyConfirmDialog';
export const confirm = createConfirmation(confirmable(MyConfirmDialog));
// ItemList.tsx
import { useState } from 'react';
import { confirm } from './confirm';
import { deleteItem } from './api';
import type { Item } from './types';
type Props = {
items: Item[];
onDeleted: (id: string) => void;
};
export function ItemList({ items, onDeleted }: Props) {
const [deletingId, setDeletingId] = useState<string | null>(null);
const handleDelete = async (item: Item) => {
const ok = await confirm({
title: '削除確認',
description: `「${item.name}」を削除します。この操作は取り消せません。`,
confirmText: '削除',
destructive: true,
});
if (!ok) return;
setDeletingId(item.id);
try {
await deleteItem(item.id);
onDeleted(item.id);
} finally {
setDeletingId(null);
}
};
return (
<ul>
{items.map((item) => (
<li key={item.id}>
<span>{item.name}</span>
<button
onClick={() => handleDelete(item)}
disabled={deletingId === item.id}
>
{deletingId === item.id ? '削除中...' : '削除'}
</button>
</li>
))}
</ul>
);
}
App root には何も追加しない。confirm は import するだけで使える。
特徴
- state: React tree 外(
createRootで動的マウントされた別 root が管理) - API: Promise
- UI: 利用側が組み立てた Dialog を
confirmable()で包む
| 長所 | 短所 |
|---|---|
| Provider セットアップ不要 | デフォルトでは Context にアクセスできない(別 root のため) |
import { confirm } from './confirm' だけ | Context が必要な場合 ContextAwareConfirmation.ConfirmationRoot を root に置く必要(実質ミニ Provider) |
window.confirm() のメンタルモデル | 別 root にマウントするため、React DevTools 上で見えにくい |
| テストで Dialog が別 tree にマウントされる癖がある |
description には JSX を渡せない場合がある点に注意(ライブラリ実装による)。Context が必要なら ContextAwareConfirmation 版を使う:
// confirm.ts (Context Aware 版)
import { confirmable, ContextAwareConfirmation } from 'react-confirm';
import { MyConfirmDialog } from './MyConfirmDialog';
export const confirm = ContextAwareConfirmation.createConfirmation(
confirmable(MyConfirmDialog)
);
// App.tsx
import { ContextAwareConfirmation } from 'react-confirm';
import { ThemeProvider } from './ThemeProvider';
export function App() {
return (
<ThemeProvider>
<ContextAwareConfirmation.ConfirmationRoot />
<YourAppContent />
</ThemeProvider>
);
}
パターン E: Render Hook(hook が JSX を返す)
LINE 証券のテックブログが初出、uhyo 氏が「render hooks」として記事化(命名は uhyo 氏の当時の上司)。日本ローカルで議論されるパターン。
実装(JSX.Element 返し版 = uhyo 氏推奨)
// useConfirm.tsx
import { useState, type ReactNode } from 'react';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
type ConfirmOptions<T> = {
title?: ReactNode;
renderDescription: (target: T) => ReactNode;
confirmText?: string;
cancelText?: string;
destructive?: boolean;
onConfirm: (target: T) => void | Promise<void>;
};
export function useConfirm<T>(options: ConfirmOptions<T>) {
const [target, setTarget] = useState<T | null>(null);
const [isPending, setIsPending] = useState(false);
const openConfirm = (item: T) => setTarget(item);
const handleConfirm = async () => {
if (!target) return;
setIsPending(true);
try {
await options.onConfirm(target);
setTarget(null);
} finally {
setIsPending(false);
}
};
const handleCancel = () => {
if (isPending) return;
setTarget(null);
};
// JSX.Element を返す(React.FC ではない)
const dialog = (
<AlertDialog
open={target !== null}
onOpenChange={(open) => {
if (!open) handleCancel();
}}
>
<AlertDialogContent>
<AlertDialogHeader>
{options.title && <AlertDialogTitle>{options.title}</AlertDialogTitle>}
<AlertDialogDescription>
{target && options.renderDescription(target)}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isPending}>
{options.cancelText ?? 'キャンセル'}
</AlertDialogCancel>
<AlertDialogAction
onClick={handleConfirm}
disabled={isPending}
className={options.destructive ? 'bg-destructive text-destructive-foreground hover:bg-destructive/90' : undefined}
>
{isPending ? '処理中...' : (options.confirmText ?? 'OK')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
return [dialog, openConfirm] as const;
}
// ItemList.tsx
import { useConfirm } from './useConfirm';
import { deleteItem } from './api';
import type { Item } from './types';
type Props = {
items: Item[];
onDeleted: (id: string) => void;
};
export function ItemList({ items, onDeleted }: Props) {
const [confirmDialog, openConfirm] = useConfirm<Item>({
title: '削除確認',
renderDescription: (item) => (
<>
「<strong>{item.name}</strong>」を削除します。この操作は取り消せません。
</>
),
confirmText: '削除',
destructive: true,
onConfirm: async (item) => {
await deleteItem(item.id);
onDeleted(item.id);
},
});
return (
<>
<ul>
{items.map((item) => (
<li key={item.id}>
<span>{item.name}</span>
<button onClick={() => openConfirm(item)}>削除</button>
</li>
))}
</ul>
{confirmDialog}
</>
);
}
特徴
- state: hook 内(実体は呼び出し元コンポーネントのフックとして登録)
- API: コールバック + JSX 返却
- UI: hook 内で JSX 構築
| 長所 | 短所 |
|---|---|
| Provider 不要、ローカルに閉じる | JSX に {confirmDialog} を置き忘れると静かに動かない |
| state と UI の凝集性が高い | UI が hook に内包され、ロジック再利用性を失う |
| 命名者の uhyo 氏自身が「あまり流行る気配がありません」と評している | |
| 英語圏の主要ライブラリでの採用例は確認できず |
重要な落とし穴: hook 内で React.FC を定義して返すと、Custom Hook 呼び出し毎にコンポーネントがアンマウント・再マウントされる(fizumi 氏の検証あり)。必ず JSX.Element を直接返すか () => JSX.Element を返して呼び出し側で {render()} する形式にする。
全パターン比較表
| A: プリミティブ | B: Provider + callback | C: Provider + Promise | D: Provider なし命令的 | E: Render Hook | |
|---|---|---|---|---|---|
| state の置き場 | 呼び出し側 | Provider | Provider + useRef | React tree 外 | hook 内(実体は呼び出し元) |
| API スタイル | props 駆動 | コールバック | Promise(await) | Promise(await) | コールバック + JSX |
| Provider 必要 | 不要 | 必要 | 必要 | 不要 | 不要 |
| Context アクセス | 自然 | 自然 | 自然 | デフォルト不可 | 自然 |
| resolve 漏れリスク | なし | なし | あり(要対策) | ライブラリが対策 | なし |
| await 跨ぎ stale closure | なし | なし | あり(要規律) | あり(要規律) | なし |
| 呼び出し側コードの線形性 | △ | △ | ◎ | ◎ | △ |
| 再利用範囲 | 各 List で重複 | アプリ全体 | アプリ全体 | アプリ全体 | hook 再利用可 |
| ライブラリ採用例 | shadcn / Radix | (自前実装が多い) | material-ui-confirm 等多数 | react-confirm 等 | 確認できず |
選択基準
| 規模・要件 | 推奨 |
|---|---|
| 1〜2 箇所だけ、自社の Dialog コンポーネントがある | A(プリミティブ + 呼び出し側 state) |
| 中規模・複数箇所、Promise の罠を避けたい | B(Provider + コールバック) |
中規模・複数箇所、await confirm() で線形に書きたい | C(Provider + Promise) |
| Provider セットアップを避けたい、Context 不要 | D(Provider なし命令的、react-confirm 等) |
| List ローカルで完結、Provider を導入したくない | A or E(Render Hook) |
| 削除以外も含めた汎用確認 UI を全社で揃えたい | B or C |
削除以外も含めた汎用確認 UI を全社で揃えたい場合、「Provider + Promise」は呼び出し側の書き味は良いが、"resolve 漏れ・stale closure 等の罠を引き受ける必要がある。"Promise の罠を避けたいなら B(コールバック方式)が筋がいい。コールバックは「処理を引数で渡す」だけのシンプルな仕組みで、Provider 側が await する設計にすることで loading 状態も Provider で一元管理できる。
material-ui-confirm 等の既存ライブラリを使う場合は実質 C 一択になるが、自前実装するなら B を選ぶ価値が十分にある。
まとめ
- 「Provider 方式 = Promise ベース」ではない。Provider + コールバック方式(パターン B)も実用的な選択肢
- 軸を分解すると 5 パターン: プリミティブ / Provider+callback / Provider+Promise / Provider なし命令的 / Render Hook
- 「Promise の罠(resolve 漏れ、await 跨ぎクロージャ)を避けたいが、Provider の統一感は欲しい」という要件には B が最適
- ライブラリ採用例の多さは
C > D > Aの順。E(Render Hook)はライブラリ採用例なし - Render Hook はライブラリ採用例が確認できず、命名者の uhyo 氏自身が「流行る気配がない」と評している
参考
- material-ui-confirm(Provider + Promise): https://github.com/jonatanklosko/material-ui-confirm
- @primer/react useConfirm(Provider + Promise): https://primer.style/product/hooks/useConfirm/
- react-confirm-hook(Provider + Promise): https://github.com/DASPRiD/react-confirm-hook
- use-confirm-hook(Provider + Promise): https://github.com/wallpants/use-confirm-hook
- react-confirm(Provider なし命令的): https://github.com/haradakunihiko/react-confirm
- shadcn/ui AlertDialog(プリミティブ): https://ui.shadcn.com/docs/components/alert-dialog
- Radix UI AlertDialog(プリミティブ): https://www.radix-ui.com/primitives/docs/components/alert-dialog
- Amir Heidari Khoram「The New Effortless useConfirm Hook」(React 18
createRootを hook 内で利用): https://amirheidarikhoram.medium.com/the-new-effortless-useconfirm-hook-for-your-confirmation-dialogs-5a38890ad9e8 - Oleksandr Dubenko「Imperative confirm dialog in React」(素朴な ReactDOM.render 利用): https://blog.dubenko.dev/confirm-dialog-react/
- uhyo「Render hooksをコンポーネントの拡張として理解する」: https://qiita.com/uhyo/items/cb6983f52ac37e59f37e
- devstation「Why You Shouldn't Put JSX in Custom React Hooks」: https://devstation.hashnode.dev/why-you-shouldnt-put-jsx-in-custom-react-hooks