TOOLS BOX/ガイド/Server Actions
Concept

Server Actions

Next.js App Router で `"use server"` を宣言したサーバー側の関数をクライアントから直接呼び出す仕組み。フォーム送信・データ変更を API Routes なしで実装でき、revalidatePath / revalidateTag / redirect と組み合わせて更新フローを完結させる。

nextjsserver-actionsuse serverformmutationrevalidateapp-router

どういう場面で使うか

  • ·フォーム送信後にサーバーでデータを作成・更新・削除するとき
  • ·API Routes を別ファイルで作らずにサーバー側の変更処理をまとめたいとき
  • ·データ変更後に revalidatePath / revalidateTag でキャッシュを即時更新したいとき
  • ·処理完了後に redirect() でページ遷移を完結させたいとき

注意点 / Pitfalls

  • ·redirect() は try/catch の外で呼ぶ。内部で throw するため catch に捕まるとリダイレクトが発動しない
  • ·引数はシリアライズ可能な値のみ(関数・クラスインスタンス・Date は渡せない)
  • ·'use server' はファイルトップレベルまたは非同期関数の先頭に宣言する。コンポーネント内に直接書く場合はインライン宣言
  • ·Server Actions は常に POST リクエスト。データ取得(GET)の代替には使えない
  • ·App Router 専用。Pages Router では使えない
  • ·Server Actions の戻り値はクライアントにシリアライズして渡されるため、DB エンティティをそのまま返さない

何と混同しやすいか

補足

更新系処理(作成・更新・削除)の標準的な実装手段。変更 → 再検証 → リダイレクトの一連のフローをサーバー側で完結させることで、API ルートの実装コストを省ける。React 19 の useActionState / useFormStatus と組み合わせてフォームの状態管理もできる。

Server Actions とは

"use server" を宣言した非同期関数をクライアントから直接呼び出せる仕組みです。フォーム送信やデータ変更を、API Routes を別途作らずに実装できます。

処理の流れは「変更 → 再検証 → リダイレクト」の 3 ステップがほとんどです。

フォーム送信 or ボタン操作
  → Server Action が呼ばれる(POST)
  → DB 更新 / バリデーション
  → revalidatePath / revalidateTag でキャッシュを更新
  → redirect() でページを遷移(または戻り値でエラーを返す)

"use server" の宣言位置

// パターン1: ファイルトップレベルに宣言(ファイル全体が Server Actions になる)
"use server";

export async function createPost(formData: FormData) { ... }
export async function deletePost(id: string) { ... }
// パターン2: 関数の先頭に宣言(Server Component 内にインラインで書く場合)
export default function Page() {
  async function handleSubmit(formData: FormData) {
    "use server";
    await db.post.create({ ... });
  }
  return <form action={handleSubmit}>...</form>;
}

フォーム送信パターン

最もシンプルな使い方は formaction に Server Action を渡すパターンです。

// app/actions.ts
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";

export async function createPostAction(formData: FormData) {
  const title = formData.get("title") as string;

  if (!title) {
    return { error: "タイトルを入力してください" };
  }

  await db.post.create({ data: { title } });
  revalidatePath("/posts");     // 一覧ページのキャッシュを更新
  redirect("/posts");           // try/catch の外で呼ぶ
}
// app/posts/new/page.tsx
import { createPostAction } from "../actions";

export default function NewPostPage() {
  return (
    <form action={createPostAction}>
      <input name="title" required />
      <button type="submit">作成</button>
    </form>
  );
}

redirect() との接点

処理完了後に遷移させたい場合は redirect()try/catch の外 で呼びます。redirect() は内部で例外を throw するため、catch に入るとリダイレクトが発動しません。

"use server";
export async function updatePostAction(id: string, formData: FormData) {
  try {
    await db.post.update({ where: { id }, data: { ... } });
    revalidatePath(`/posts/${id}`);
  } catch (e) {
    return { error: "更新に失敗しました" };
  }
  redirect(`/posts/${id}`); // try の外
}

revalidatePath / revalidateTag との接点

データ変更後のキャッシュ更新は Server Actions の中で revalidatePath / revalidateTag を呼びます。

"use server";
import { revalidatePath, revalidateTag } from "next/cache";

export async function deletePostAction(id: string) {
  await db.post.delete({ where: { id } });

  // パス単位で更新(シンプル)
  revalidatePath("/posts");

  // タグ単位で更新(複数ページにまたがる場合)
  revalidateTag("posts");
}

useActionState でエラーと状態を受け取る(React 19)

useActionState を使うと Server Action の戻り値をクライアントで受け取れます。バリデーションエラーの表示やローディング状態の管理に使います。

"use client";
import { useActionState } from "react";
import { createPostAction } from "./actions";

type ActionState = { error?: string } | null;

export function CreatePostForm() {
  const [state, action, isPending] = useActionState<ActionState, FormData>(
    createPostAction,
    null
  );

  return (
    <form action={action}>
      {state?.error && <p className="text-red-500">{state.error}</p>}
      <input name="title" required />
      <button type="submit" disabled={isPending}>
        {isPending ? "作成中…" : "作成"}
      </button>
    </form>
  );
}

誤用しやすいポイント

データ取得に使う

// ❌ Server Actions は POST 専用。データ取得には使えない
export async function getPostsAction() {
  return await db.post.findMany(); // これはやらない
}

// ✅ データ取得は Server Component で直接行う
export default async function PostsPage() {
  const posts = await db.post.findMany();
  return <PostList posts={posts} />;
}

redirect() を try/catch の中に入れる

// ❌ redirect() がキャッチされてリダイレクトされない
export async function myAction(formData: FormData) {
  try {
    await doSomething();
    redirect("/done"); // NEXT_REDIRECT が catch に入る
  } catch (e) {
    return { error: "失敗" };
  }
}

// ✅ try/catch の外で redirect() を呼ぶ
export async function myAction(formData: FormData) {
  try {
    await doSomething();
  } catch (e) {
    return { error: "失敗" };
  }
  redirect("/done");
}

関数・Date をそのまま引数に渡す

// ❌ シリアライズできない値は渡せない
someAction(new Date(), () => {}, instance);

// ✅ シリアライズ可能な値(文字列・数値・プレーンオブジェクト)を渡す
someAction(date.toISOString(), config.id);

redirect() の詳細は → redirect

キャッシュ再検証の詳細は → Next.js キャッシュ設計

React 19 Hook との組み合わせ → useActionState / useFormStatus / useOptimistic

失敗の種類と扱い方 → エラーハンドリングの考え方

関連ドキュメント

関連サンプル

同じテーマや技術スタックを使った実装例