こんにちは、かつコーチです。
前回は "use client" をいつ・どこに書くべきか、判断基準を解説しました。
Server ComponentsとClient Componentsの使い分けが分かってくると、次に気になるのが「フォームを送信して、サーバー側でデータを保存する処理はどう書くのか」という点です。
今回は、API Routeを作らずにサーバー処理を書けるServer Actionsの基本を解説します。
Server Actionsとは?
サーバー側で実行される関数
Server Actionsとは、"use server" というディレクティブを付けることで、ブラウザから直接呼び出せる「サーバー専用の関数」を作れる仕組みです。
これまでフォーム送信を処理するには、app/api/xxx/route.ts のようなAPI Routeを別途作り、fetch で呼び出すのが一般的でした。
Server Actionsを使うと、そのAPI Routeを作る手間を省き、フォームの action 属性に関数を直接渡せます。
なぜそれが必要なのか
API Routeを都度作る方式は、リクエストの型・レスポンスの型・エラーハンドリングをフロントとバックエンドの両方に書く必要があり、コード量が増えがちでした。
Server Actionsを使うと、サーバー側の関数をそのままクライアントから呼び出せるため、型情報を共有しながら、より少ないコードでフォーム処理を書けます。
さらに、JavaScriptが読み込まれる前でもフォーム送信自体は機能するプログレッシブエンハンスメントにも対応しています。
基本の書き方
手順1:’use server’でServer Actionを定義する
// app/actions/createTodo.ts
"use server";
export async function createTodo(formData: FormData) {
const title = formData.get("title") as string;
if (!title) {
throw new Error("タイトルは必須です");
}
// 本来はここでデータベースに保存する
await db.todo.create({ data: { title } });
}
ファイルの一番上に "use server" を書くことで、この中の関数がServer Actionとして扱われます。
手順2:フォームのaction属性に渡す
// app/todos/page.tsx(Server Component)
import { createTodo } from "../actions/createTodo";
export default function TodosPage() {
return (
<form action={createTodo}>
<input type="text" name="title" placeholder="やること" />
<button type="submit">追加</button>
</form>
);
}
通常のHTMLの <form> とほぼ同じ書き方のまま、action にServer Actionの関数を渡すだけで動きます。
onSubmit や useState を使う必要がないため、この page.tsx はServer Componentのままにできます。
手順3:処理後に画面を更新する
データを追加しただけでは、一覧画面に反映されません。
// app/actions/createTodo.ts
"use server";
import { revalidatePath } from "next/cache";
export async function createTodo(formData: FormData) {
const title = formData.get("title") as string;
if (!title) throw new Error("タイトルは必須です");
await db.todo.create({ data: { title } });
revalidatePath("/todos"); // 一覧ページのキャッシュを再検証
}
revalidatePath を呼ぶことで、指定したパスのキャッシュが再検証され、最新のデータが画面に反映されます。
つまずきやすい設定・注意点
Server Actionに渡せる引数は、文字列・数値・FormData などシリアライズ可能な値に限られ、関数やクラスインスタンスは渡せません。
よくあるつまずきポイント・エラー対処
一次情報:revalidatePathを忘れて「保存したのに反映されない」と焦った話
私が初めてServer Actionsを使ったとき、データ自体はデータベースに保存されているのに、画面には反映されず「保存に失敗している」と勘違いして時間を溶かしたことがあります。
❌ Before:revalidatePathを書き忘れる
// app/actions/createTodo.ts
"use server";
export async function createTodo(formData: FormData) {
const title = formData.get("title") as string;
if (!title) throw new Error("タイトルは必須です");
await db.todo.create({ data: { title } });
// revalidatePathを呼んでいない
}
データベースの管理画面で確認すると、実はレコードはきちんと追加されていました。
原因は、Next.jsがページの表示速度を上げるために page.tsx のレンダリング結果をキャッシュしており、revalidatePath を呼ばない限りそのキャッシュが更新されないことでした。
✅ After:revalidatePathでキャッシュを再検証する
// app/actions/createTodo.ts
"use server";
import { revalidatePath } from "next/cache";
export async function createTodo(formData: FormData) {
const title = formData.get("title") as string;
if (!title) throw new Error("タイトルは必須です");
await db.todo.create({ data: { title } });
revalidatePath("/todos");
}
revalidatePath("/todos") を1行足すだけで、送信後に一覧ページが最新の状態で再表示されるようになりました。
「保存処理そのもの」と「画面のキャッシュ更新」は別物だと意識しておくと、この種の勘違いを防げます。
まとめ
この記事のポイント
- Server Actionsは
"use server"を付けた関数を、クライアントから直接呼び出せる仕組み - フォームの
action属性に関数を渡すだけで、API Routeを作らずに送信処理を書ける - Server Actionの引数はシリアライズ可能な値(文字列・数値・FormDataなど)に限られる
- データ更新後は
revalidatePathを呼ばないと、画面のキャッシュが更新されない
次に読むべき記事
ここまでで、ルーティングからServer Actionsまで、App Routerの基本的な機能を一通り押さえられました。
次回は少し実践的な内容として、Client Componentの中にServer Componentを混ぜてバンドルが膨らんだ話を解説します。
→ 次の記事:Client Componentの中にServer Componentを混ぜてバンドルが膨らんだ話