第146章:成功/失敗のトースト通知を付ける🔔✨
フォーム送信って、成功したのか失敗したのか分かりにくいと不安だよね🥺 そこで今回は Server Actionsの結果に合わせて「トースト(ふわっと出る通知)」 を出して、体験を一気に良くするよ〜!🔔💖
今日はこれを作るよ🧁
- 送信成功 → ✅「できたよ!」トースト
- 送信失敗 → ❌「ごめん、失敗…」トースト
- Server Actionsはサーバーで動くので、トースト表示は クライアント側でやるよ🙆♀️
useActionState は「state」「formに渡すaction」「pending(送信中か)」の3つが取れるよ。 (React)
ざっくり流れ図🗺️(これができれば勝ち!)
Step 1:トーストライブラリを入れる🍞✨(sonner)
今回は軽くて人気な sonner を使うよ〜🔔
ターミナルで👇
npm i sonner
(sonnerは <Toaster /> をどこかに置いて、toast() を呼ぶタイプだよ) (Shadcn UI)
Step 2:Toaster(通知の置き場所)をアプリに常設する🏠🔔
App Routerの layout.tsx は基本サーバー側だから、ToasterはClient Componentに分けて置くのが安全だよ🙆♀️
app/_components/ToasterProvider.tsx
"use client";
import { Toaster } from "sonner";
export function ToasterProvider() {
return <Toaster position="top-right" richColors />;
}
app/layout.tsx(末尾に追加)
import type { ReactNode } from "react";
import { ToasterProvider } from "./_components/ToasterProvider";
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="ja">
<body>
{children}
<ToasterProvider />
</body>
</html>
);
}
Step 3:Server Actionは「結果を返す」だけにする📦🧑🍳
useActionState を使うと、Server Actionの引数が (prevState, formData) になるよ。 (Next.js)
ここで 成功/失敗とメッセージを返すようにするのがポイント✨
app/actions/todo.ts
"use server";
export type AddTodoState = {
ok: boolean | null; // null=初期状態
message: string;
key: number; // トースト重複防止用
};
export async function addTodo(
prevState: AddTodoState,
formData: FormData
): Promise<AddTodoState> {
const title = String(formData.get("title") ?? "").trim();
if (!title) {
return { ok: false, message: "タイトルが空だよ…🥲", key: Date.now() };
}
try {
// 例:DB保存の代わりにちょい待つ(雰囲気)
await new Promise((r) => setTimeout(r, 300));
return { ok: true, message: `「${title}」を追加したよ🎉`, key: Date.now() };
} catch {
return { ok: false, message: "保存に失敗しちゃった…もう一回🙏", key: Date.now() };
}
}
Step 4:クライアント側で state を見てトースト出す🔔💖
Server Actionはサーバーで動くから、toast.success / toast.error はクライアントで呼ぶよ😊
useEffect で state の変化を見て出すのが王道✨
※「同じstateで何回もトースト出ちゃう😭」を避けるために、key を返して 前回と違う時だけ出すようにするよ(このやり方よく使われるよ〜) (Stack Overflow)
app/_components/TodoAddForm.tsx
"use client";
import { useActionState, useEffect, useRef } from "react";
import { toast } from "sonner";
import { addTodo, type AddTodoState } from "@/app/actions/todo";
const initialState: AddTodoState = { ok: null, message: "", key: 0 };
export function TodoAddForm() {
const [state, formAction, pending] = useActionState(addTodo, initialState);
const lastKeyRef = useRef(0);
useEffect(() => {
if (!state.key || state.key === lastKeyRef.current) return;
lastKeyRef.current = state.key;
if (state.ok) toast.success(state.message);
else toast.error(state.message);
}, [state]);
return (
<form action={formAction} style={{ display: "flex", gap: 8, alignItems: "center" }}>
<input
name="title"
placeholder="TODOを入力…📝"
style={{ padding: "8px 10px", border: "1px solid #ccc", borderRadius: 8 }}
/>
<button
type="submit"
disabled={pending}
style={{ padding: "8px 12px", border: "1px solid #ccc", borderRadius: 8 }}
>
{pending ? "送信中…⏳" : "追加➕"}
</button>
</form>
);
}
Step 5:ページに置いて動作チェック✅🎉
app/page.tsx
import { TodoAddForm } from "./_components/TodoAddForm";
export default function Page() {
return (
<main style={{ padding: 24 }}>
<h1>TODO 追加🧸</h1>
<TodoAddForm />
</main>
);
}
- 入力して送信 → ✅トースト🎉
- 空で送信 → ❌トースト🥲
よくあるハマり集🪤(ここだけ押さえよ!)
-
トーストが出ない
<ToasterProvider />をlayout.tsxに置き忘れがち🥺
-
Server Action内で toast しようとする
- サーバーにはブラウザがないから無理だよ🙅♀️(結果を返して、クライアントでtoast!)
-
redirect() するとトースト出しづらい
- Server Actionで
redirect()すると「結果state」を受け取りにくいことがあるよ。トースト出したいなら、まずは redirectしない設計でOK🙆♀️(慣れたら別パターンもできる!)
- Server Actionで
できたらミニ課題🎀
- 成功トーストを「🎉 追加できたよ!」みたいに絵文字マシマシにする🤣
- 失敗トーストを「⚠️ もう一回だけお願い…!」みたいに優しくする🫶
positionを"bottom-center"に変えて好みチェック👀
この章は以上だよ〜!🔔✨