
【Next.js】削除・更新結果をトースト通知で表示する
こんにちは、ライトです。
これまで、管理画面で使うコンポーネントとして以下のようなものを作成してきました。
管理画面レイアウト
StatusBadge
DataTable
Pagination
SearchFilterBar
RowActionMenu
ConfirmModal
前回は、削除確認や更新確認などに使える ConfirmModal を作成しました。
ConfirmModal は、ユーザーに対して「本当に実行してよいか」を確認するためのコンポーネントです。
たとえば、削除操作であれば次のような流れになります。
削除するをクリック
ConfirmModalを表示
削除してよいか確認
削除処理を実行
ただ、確認して処理を実行したあとには、ユーザーに結果を伝える必要があります。
削除しました
削除に失敗しました
更新しました
更新に失敗しました
このような操作結果の通知には、以前作成した Toast コンポーネントが使えます。
今回は、ConfirmModal で削除や更新を確認し、処理結果を Toast で表示する仕組みを作ります。
ConfirmModalとToastの役割
まず、ConfirmModal と Toast の役割を整理します。
ConfirmModal
操作前の確認Toast
操作後の結果通知
たとえば、削除処理なら次のようになります。
本当に削除しますか?
→ ConfirmModal削除しました
→ Toast
このように役割を分けると、UIの意味が分かりやすくなります。
以前作成したToastを管理画面向けに少し拡張する
以前作成したToastは、以下のように使える形でした。
const { showToast } = useToastContext();
showToast({
type: "success",
message: "保存しました。",
});このままでも削除や更新の通知に使えます。
showToast({
type: "success",
message: "ユーザーを削除しました。",
});ただ、管理画面では通知内容をもう少し分かりやすくしたいことがあります。
たとえば、以下のように タイトル と 本文 を分けたい場合です。

また、エラー通知は成功通知より少し長めに表示したいこともあります。
そこで今回は、Toastに以下を追加します。
title
通知のタイトルduration
表示時間
Toastの型を更新する
まず、Toastの型に title と duration を追加します。
components/Toast/Toast.tsx
import styles from "./Toast.module.css";
export type ToastType = "success" | "error" | "warning" | "info";
export type ToastItem = {
id: number;
type: ToastType;
title?: string;
message: string;
duration?: number;
};
type ToastProps = {
toast: ToastItem;
onClose: (id: number) => void;
};
const toastLabels: Record<ToastType, string> = {
success: "成功",
error: "エラー",
warning: "警告",
info: "通知",
};
export default function Toast({ toast, onClose }: ToastProps) {
return (
<div className={`${styles.toast} ${styles[toast.type]}`} role="status">
<div className={styles.content}>
<div className={styles.label}>{toastLabels[toast.type]}</div>
{toast.title && (
<div className={styles.title}>
{toast.title}
</div>
)}
<div className={styles.message}>{toast.message}</div>
</div>
<button
type="button"
className={styles.closeButton}
onClick={() => onClose(toast.id)}
aria-label="通知を閉じる"
>
×
</button>
</div>
);
}追加したのはこの部分です。
title?: string;
duration?: number;title は任意なので、これまで通り message だけでも使えます。
showToast({
type: "success",
message: "保存しました。",
});タイトルを付けたい場合は、次のように使います。
showToast({
type: "success",
title: "削除しました",
message: "山田 太郎を削除しました。",
});Toast.module.cssを更新する
title を表示するために、CSSも少し追加します。
components/Toast/Toast.module.css
.container {
position: fixed;
z-index: 1000;
display: flex;
flex-direction: column;
gap: 12px;
width: min(360px, calc(100vw - 32px));
pointer-events: none;
}
.topRight {
top: 24px;
right: 24px;
}
.topLeft {
top: 24px;
left: 24px;
}
.bottomRight {
right: 24px;
bottom: 24px;
}
.bottomLeft {
bottom: 24px;
left: 24px;
}
.topCenter {
top: 24px;
left: 50%;
transform: translateX(-50%);
}
.bottomCenter {
bottom: 24px;
left: 50%;
transform: translateX(-50%);
}
.toast {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
padding: 14px 14px 14px 16px;
border: 1px solid;
border-radius: 14px;
background: #ffffff;
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.14);
animation: slideIn 0.24s cubic-bezier(0.16, 1, 0.3, 1);
pointer-events: auto;
}
.content {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.label {
font-size: 11px;
font-weight: 700;
line-height: 1;
opacity: 0.72;
}
.title {
font-size: 14px;
font-weight: 700;
line-height: 1.4;
}
.message {
font-size: 14px;
line-height: 1.5;
}
.closeButton {
width: 24px;
height: 24px;
flex-shrink: 0;
border: none;
border-radius: 999px;
background: transparent;
color: currentColor;
cursor: pointer;
font-size: 18px;
line-height: 1;
opacity: 0.7;
transition:
background 0.2s ease,
opacity 0.2s ease;
}
.closeButton:hover {
background: rgba(15, 23, 42, 0.08);
opacity: 1;
}
.success {
border-color: #86efac;
background: #f0fdf4;
color: #166534;
}
.error {
border-color: #fecaca;
background: #fef2f2;
color: #991b1b;
}
.warning {
border-color: #fde68a;
background: #fffbeb;
color: #92400e;
}
.info {
border-color: #a5f3fc;
background: #ecfeff;
color: #155e75;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-8px) scale(0.98);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}今回追加した主なクラスは title です。
.title {
font-size: 14px;
font-weight: 700;
line-height: 1.4;
}通知タイトルを少し太く表示することで、内容を読み取りやすくしています。
ToastProviderを更新する
次に、Context APIで作成していた ToastProvider 側も更新します。
showToast で title と duration を受け取れるようにします。
components/Toast/ToastProvider.tsx
"use client";
import {
createContext,
useCallback,
useContext,
useState,
} from "react";
import ToastContainer, { ToastPosition } from "./ToastContainer";
import { ToastItem, ToastType } from "./Toast";
type ShowToastParams = {
type: ToastType;
title?: string;
message: string;
duration?: number;
};
type ToastContextValue = {
showToast: (params: ShowToastParams) => void;
};
type ToastProviderProps = {
children: React.ReactNode;
position?: ToastPosition;
};
const ToastContext = createContext<ToastContextValue | null>(null);
export function ToastProvider({
children,
position = "top-right",
}: ToastProviderProps) {
const [toasts, setToasts] = useState<ToastItem[]>([]);
const removeToast = useCallback((id: number) => {
setToasts((currentToasts) =>
currentToasts.filter((toast) => toast.id !== id)
);
}, []);
const showToast = useCallback(
({ type, title, message, duration = 3000 }: ShowToastParams) => {
const id = Date.now();
const newToast: ToastItem = {
id,
type,
title,
message,
duration,
};
setToasts((currentToasts) => [...currentToasts, newToast]);
window.setTimeout(() => {
removeToast(id);
}, duration);
},
[removeToast]
);
return (
<ToastContext.Provider value={{ showToast }}>
{children}
<ToastContainer
toasts={toasts}
position={position}
onClose={removeToast}
/>
</ToastContext.Provider>
);
}
export function useToastContext() {
const context = useContext(ToastContext);
if (!context) {
throw new Error("useToastContext must be used within ToastProvider");
}
return context;
}ShowToastParams に title と duration を追加しています。
type ShowToastParams = {
type: ToastType;
title?: string;
message: string;
duration?: number;
};duration は指定しない場合、3秒にしています。
duration = 3000成功通知は短め、エラー通知は長めにするなど、通知ごとに調整できます。
showToast({
type: "error",
title: "削除に失敗しました",
message: "時間をおいて再度お試しください。",
duration: 5000,
});layout.tsxでToastProviderを設定する
ToastProvider は、アプリ全体で使えるように app/layout.tsx に設定しておきます。
app/layout.tsx
import { ToastProvider } from "@/components/Toast";
import type { Metadata } from "next";
import { Noto_Sans_JP } from "next/font/google";
import "./globals.css";
const notoSansJP = Noto_Sans_JP({
subsets: ["latin"],
weight: ["400", "500", "600", "700"],
display: "swap",
});
export const metadata: Metadata = {
title: "Admin Dashboard",
description: "管理画面ダッシュボード",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ja">
<body className={`${notoSansJP.className} antialiased`}>
<ToastProvider position="top-right">
{children}
</ToastProvider>
</body>
</html>
);
}これで、Provider配下のどのコンポーネントからでも showToast() を呼び出せます。
const { showToast } = useToastContext();削除結果をToastで表示する
次に、削除処理の結果をToastで表示します。
前回作成した ConfirmModal と RowActionMenu を組み合わせたユーザー一覧を例にします。
const { showToast } = useToastContext();削除処理では、成功時と失敗時でToastを出し分けます。
const handleDelete = async () => {
if (!selectedUser) {
return;
}
setIsDeleting(true);
try {
await new Promise((resolve) => setTimeout(resolve, 800));
setUsers((currentUsers) =>
currentUsers.filter((user) => user.id !== selectedUser.id)
);
showToast({
type: "success",
title: "削除しました",
message: `${selectedUser.name}を削除しました。`,
});
setIsDeleteModalOpen(false);
setSelectedUser(null);
} catch {
showToast({
type: "error",
title: "削除に失敗しました",
message: "時間をおいて再度お試しください。",
duration: 5000,
});
} finally {
setIsDeleting(false);
}
};成功した場合は、成功通知を表示します。
showToast({
type: "success",
title: "削除しました",
message: `${selectedUser.name}を削除しました。`,
});失敗した場合は、エラー通知を表示します。
showToast({
type: "error",
title: "削除に失敗しました",
message: "時間をおいて再度お試しください。",
duration: 5000,
});エラー通知は、内容を読んでもらいたいので少し長めに表示しています。
更新結果をToastで表示する
更新確認でも同じように使えます。
const handleUpdate = async () => {
setIsSaving(true);
try {
await new Promise((resolve) => setTimeout(resolve, 800));
showToast({
type: "success",
title: "更新しました",
message: "変更内容を保存しました。",
});
setIsUpdateModalOpen(false);
} catch {
showToast({
type: "error",
title: "更新に失敗しました",
message: "入力内容を確認して、再度お試しください。",
duration: 5000,
});
} finally {
setIsSaving(false);
}
};成功時は success、失敗時は error を使います。
showToast({
type: "success",
title: "更新しました",
message: "変更内容を保存しました。",
});showToast({
type: "error",
title: "更新に失敗しました",
message: "入力内容を確認して、再度お試しください。",
duration: 5000,
});ConfirmModal は削除確認だけでなく、更新確認にも使えるので、Toastと組み合わせると使い回しやすくなります。
ConfirmModalと組み合わせる
削除確認の ConfirmModal は、次のように使います。
<ConfirmModal
isOpen={isDeleteModalOpen}
title="ユーザーを削除しますか?"
description="この操作は取り消せません。削除すると、ユーザー情報は一覧から削除されます。"
confirmLabel="削除する"
cancelLabel="キャンセル"
variant="danger"
isLoading={isDeleting}
onConfirm={handleDelete}
onClose={closeDeleteModal}
>
{selectedUser && (
<div className="rounded-lg border border-slate-200 bg-slate-50 p-3 text-sm text-slate-700">
<div className="font-medium text-slate-900">
{selectedUser.name}
</div>
<div className="mt-1 text-slate-500">
{selectedUser.email}
</div>
</div>
)}
</ConfirmModal>この流れになります。
RowActionMenuで削除を選択
ConfirmModalを開く
削除するをクリック
handleDeleteを実行
成功・失敗に応じてToastを表示
ConfirmModal は操作前の確認、Toast は操作後の結果通知です。
RowActionMenuから削除確認を開く
RowActionMenu 側では、削除をクリックしたときに対象ユーザーをセットし、確認モーダルを開きます。
<RowActionMenu
label={`${user.name}の操作`}
actions={[
{
label: "詳細を見る",
onClick: () => {
console.log("詳細:", user.id);
},
},
{
label: "編集する",
onClick: () => {
console.log("編集:", user.id);
},
},
{
label: "削除する",
variant: "danger",
onClick: () => {
setSelectedUser(user);
setIsDeleteModalOpen(true);
},
},
]}
/>この時点では、まだ削除処理は実行しません。
削除処理は、ConfirmModal の実行ボタンを押したときに行います。
onConfirm={handleDelete}これにより、誤操作を防ぎやすくなります。
全体の流れ
ここまでをまとめると、全体の流れは次のようになります。
DataTable
RowActionMenu
ConfirmModal
削除・更新処理
Toast通知
それぞれの役割は以下です。
DataTable
一覧を表示するRowActionMenu
行ごとの操作を表示するConfirmModal
操作前に確認するToast
操作後の結果を通知する
このように役割を分けると、各コンポーネントの責務が分かりやすくなります。
実装のポイント
今回のポイントは、既存のToastを管理画面向けに少し拡張したことです。
title
通知の見出しmessage
通知の本文duration
表示時間
これにより、操作結果をより分かりやすく伝えられます。
showToast({
type: "success",
title: "削除しました",
message: "山田 太郎を削除しました。",
});また、ConfirmModalとの役割分担も重要です。
ConfirmModal
実行前の確認Toast
実行後の結果通知
削除や更新のような処理では、この2つを組み合わせると、ユーザーにとって分かりやすい操作フローになります。
まとめ
今回は、削除・更新結果をトースト通知で表示する仕組みを作成しました。
以前作成したToastコンポーネントをベースに、管理画面向けに以下を追加しました。
title
duration
これにより、通知の見出しと本文を分けたり、通知ごとに表示時間を変えたりできるようになりました。
また、削除や更新の流れは次のように整理しました。
RowActionMenu
操作を選ぶConfirmModal
実行前に確認するToast
実行後の結果を表示する
管理画面では、削除、更新、公開、停止など、確認が必要な操作が多くあります。
ConfirmModal と Toast を組み合わせることで、操作前の確認と操作後の通知を分けて扱えるようになります。
これで、管理画面の操作フローがかなり実務に近づいてきました。