Skip to main content

React 削除確認ダイアログの設計パターン整理 - 5 パターンの完全実装比較

TL;DR​

  • 実装方式は 3 つの軸の組み合わせ: state の置き場(呼び出し側 / Provider / React tree 外)、呼び出し API(props / コールバック / Promise)、UI の提供形式(ライブラリ提供 / 利用側組み立て)
  • 実在する組み合わせは大きく 5 パターン:
    1. プリミティブ + 呼び出し側 state(shadcn / Radix)
    2. Provider + コールバック方式(showConfirm({onConfirm, onCancel}))
    3. Provider + Promise 方式(await confirm({...}) - material-ui-confirm 系)
    4. Provider なし命令的 API(react-confirm 系、createRoot で動的マウント)
    5. 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 + callbackC: Provider + PromiseD: Provider なし命令的E: Render Hook
state の置き場呼び出し側ProviderProvider + useRefReact 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 氏自身が「流行る気配がない」と評している

参考​