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>;
}
フォーム送信パターン
最もシンプルな使い方は form の action に 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
失敗の種類と扱い方 → エラーハンドリングの考え方