🐢

Next.jsでフラッシュメッセージを実装する(App Router)

2025/01/01に公開

これは何

  • Next.jsApp Routerを使ってフラッシュメッセージを実装する方法の紹介です
  • 環境
    • Next 15.1.2
    • React 19.0.0

ユースケース

以下のリストテーブルの右側のごみ箱アイコンをクリックしたら行を削除し、画面の右下にフラッシュメッセージ(トーストメッセージ)を表示します

動画

実装方針

  • ごみ箱アイコンがクリックされたらアイコンも含めて行が消えるので、アイコンのコンポーネントに表示ロジックは持たせないようにしました
  • 状態のバケツリレーを避けたかったので、Nextのcookiesを使ってページ層で一括管理するようにしました
  • UIコンポーネントはshadcn/uiのSonnerを使いましたが、ネットで調べた記事によく出てきたから使ったのであって特にこだわりはありません

メッセージ表示コンポーネント

"use client";

import { ReactNode, useEffect } from "react";
import { toast } from "sonner";

export function Notify({
  children,
  isSuccessDeleteInvoice,
}: {
  children: ReactNode;
  isSuccessDeleteInvoice: boolean;
}) {
  useEffect(() => {
    if (isSuccessDeleteInvoice) {
      toast.success("Delete invoice successfully.");
    }
  });

  return <>{children}</>;
}

表示コンポーネントは状態を持つのでクライアントコンポーネントとして切り出し、サーバーコンポーネントから呼び出すようにしました。メインコンテンツをラップするようなコンポーネントです。

メッセージコンポーネントの埋め込み

import Table from "@/app/ui/invoices/table";
import { Notify } from "@/app/ui/invoices/notify";
import { cookies } from "next/headers";

export default async function Page() {
  const isSuccessDeleteInvoice = (await cookies()).has("successDeleteInvoice");

  return (
    <Notify isSuccessDeleteInvoice={isSuccessDeleteInvoice}>
      <Table />
    </Notify>
  );
}

ページ層に、メインで表示したいコンポーネントである<Table />の親としてメッセージコンポーネントを設定します。メッセージ表示を実際に行うUIコンポーネントはapp/layout.tsxに埋め込んでおり、Notifyコンポーネントのtoast()が実行されると表示される仕組みです。

データ削除ロジック

"use server";

import { PrismaClient } from "@prisma/client";
import { revalidatePath } from "next/cache";
import { cookies } from "next/headers";

const prisma = new PrismaClient();

export async function deleteInvoice(id: string, _prevState: unknown) {
  await prisma.invoices.delete({
    where: { id: id },
  });
  (await cookies()).set("successDeleteInvoice", "true", { maxAge: 0 });
  revalidatePath("/dashboard/invoices");
}

FormDataを使わず、明示的にパラメータを渡す形にしたのでuseActionState用の_prevStateはパラメータの次に書いてます(今回は状態管理しないのでunknown型にしてます)。revalidatePathでページの再検証をする前にmaxAge: 0のクッキーをセットし、ページ層で一度だけ検証したら即時消失するようにします。

データ削除ボタンコンポーネント

"use client";

import { TrashIcon } from "@heroicons/react/24/outline";
import { deleteInvoice } from "@/app/lib/actions";
import { useActionState } from "react";

export function DeleteInvoice({ id }: { id: string }) {
  const deleteInvoiceWithId = deleteInvoice.bind(null, id);
  const [_state, formAction, pending] = useActionState(
    deleteInvoiceWithId,
    undefined
  );

  return (
    <form action={formAction}>
      <button
        className="rounded-md border p-2 hover:bg-gray-100"
        disabled={pending}
      >
        <span className="sr-only">Delete</span>
        <TrashIcon className="w-5" />
      </button>
    </form>
  );
}

フォームで状態表示をしないので_stateとしましたが、もしかしたらuseActionStateはオーバーエンジニアリングだったかも(型エラーが出るのが怖かったのでサーバーアクション使うからuseActionStateを使うと作法的に思考しました)

以上です

感想

Railsのようにコントローラでフラッシュメッセージを設定できる作りは便利だと思いましたのでやってみました。Next.jsでどうやるのがベストプラクティスなのかは分かってませんが、おそらくもっと良い方法があると思います。

Discussion