前回の記事ではRoute Handlerを使ってAPIエンドポイントを作り、フロントエンドから fetch() で呼び出しました。
処理の流れを振り返ると、こうなっていました。
フォーム(Client Component)
→ fetch('/api/posts', { method: 'POST', ... })
→ Route Handler(app/api/posts/route.ts)
→ データを処理して返すこれはこれで正しい実装です。
ただ、
Next.jsにはもう一つのアプローチがあります。
Server Actions を使うと、APIエンドポイントを経由せずに、フォームからサーバー側の関数を直接呼び出せます。
フォーム(Client Component)
→ Server Action(サーバー上の関数)を直接呼び出す
→ データを処理する中間のAPIが不要になるぶん、コードがすっきりします。
今回はこのServer Actionsをフォームと組み合わせて学びます。
Contents
Server Actionsの基本
Server Actionは "use server" ディレクティブを付けた非同期関数です。
定義の仕方は2通りあります。
方法①:ファイルの先頭に "use server" を書く
// actions/post.ts
"use server";
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const body = formData.get('body') as string;
console.log('サーバーで受け取ったデータ:', { title, body });
// ここにデータベースへの保存処理などを書く
}ファイル全体がServer Actionになります。
複数のServer Actionをまとめて管理したいときに便利です。
方法②:関数の中に "use server" を書く
// Server Component の中でインラインで定義する
export default function Page() {
async function handleSubmit(formData: FormData) {
"use server";
const title = formData.get('title') as string;
console.log('タイトル:', title);
}
return (
<form action={handleSubmit}>
<input name="title" />
<button type="submit">送信</button>
</form>
);
}小さなコンポーネントで使い捨てのServer Actionを書くときに向いています。
どちらの書き方でも動作は同じです。
この記事では管理しやすい方法①(専用ファイル)をメインで使っていきます。
フォームとServer Actionを繋げる
<form> タグの action 属性にServer Actionを渡すのが基本の繋ぎ方です。
my-first-nextjs/
├── actions/ ← app/ と同列のプロジェクトルート直下
│ └── post.ts ← Server Actionをまとめるファイル
├── app/
│ └── posts/
│ └── new/
│ └── page.tsx ← フォームページ
└── components/// actions/post.ts
"use server";
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const body = formData.get('body') as string;
if (!title || !body) {
return { error: 'タイトルと本文は必須です' };
}
// 実際のアプリではここでDBに保存する
console.log('新しい記事:', { title, body });
return { success: true };
}⚠️ サーバーサイドで動くのでconsole.log()の結果は、ターミナルに表示されます。
// app/posts/new/page.tsx
import { createPost } from '@/actions/post';
export default function NewPostPage() {
return (
<main>
<h1>新しい記事を投稿</h1>
<form action={createPost} style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem', maxWidth: '500px' }}>
<input
name="title"
placeholder="タイトル"
required
style={{ padding: '0.5rem', border: '1px solid #ccc', borderRadius: '4px' }}
/>
<textarea
name="body"
placeholder="本文"
rows={4}
required
style={{ padding: '0.5rem', border: '1px solid #ccc', borderRadius: '4px' }}
/>
<button
type="submit"
style={{ padding: '0.5rem 1rem', background: '#0070f3', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer' }}
>
投稿する
</button>
</form>
</main>
);
}<form action={createPost}> と書くだけで、送信ボタンを押したときにServer Actionが呼ばれます。
前回のRoute Handler版と比べると、fetch() を書く必要がなく、コードがかなりスッキリしています。
useActionState でフィードバックを表示する
フォームを送信した後、「成功した」「エラーが起きた」という結果をユーザーに伝えたいですよね。
そのために使うのが useActionState フックです。
useActionState はReactのHooksなので、使うコンポーネントはClient Componentにする必要があります。
useActionStateは簡潔に「送信中かどうか」の真偽値を監視できたり、stateで通信結果の管理できます。
// components/CreatePostForm.tsx
"use client";
import { useActionState } from 'react';
import { createPost } from '@/actions/post';
type ActionResult = {
error?: string;
success?: boolean;
} | null;
export default function CreatePostForm() {
const [state, formAction, isPending] = useActionState<ActionResult, FormData>(
createPost,
null
);
return (
<form action={formAction} style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem', maxWidth: '500px' }}>
<h2>新しい記事を投稿</h2>
{/* 成功メッセージ */}
{state?.success && (
<p style={{ color: 'green', padding: '0.5rem', background: '#f0fff4', borderRadius: '4px' }}>
✅ 記事を投稿しました!
</p>
)}
{/* エラーメッセージ */}
{state?.error && (
<p style={{ color: 'red', padding: '0.5rem', background: '#fff5f5', borderRadius: '4px' }}>
❌ {state.error}
</p>
)}
<input
name="title"
placeholder="タイトル"
style={{ padding: '0.5rem', border: '1px solid #ccc', borderRadius: '4px' }}
/>
<textarea
name="body"
placeholder="本文"
rows={4}
style={{ padding: '0.5rem', border: '1px solid #ccc', borderRadius: '4px' }}
/>
<button
type="submit"
disabled={isPending}
style={{
padding: '0.5rem 1rem',
background: isPending ? '#aaa' : '#0070f3',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: isPending ? 'not-allowed' : 'pointer',
}}
>
{isPending ? '投稿中...' : '投稿する'}
</button>
</form>
);
}useActionState が返す3つの値を整理します。
| 変数 | 内容 |
|---|---|
state | Server Actionが返した値(初期値は null) |
formAction | <form action> に渡す関数 |
isPending | 送信中かどうかの真偽値 |
isPending を使ってボタンを disabled にすることで、二重送信も防げます。
Server Action側もuseActionStateに合わせて修正
useActionState を使う場合、Server Actionの引数に prevState(前回の状態)が加わります。
// actions/post.ts
"use server";
type ActionResult = {
error?: string;
success?: boolean;
} | null;
export async function createPost(
prevState: ActionResult,
formData: FormData
): Promise<ActionResult> {
const title = formData.get('title') as string;
const body = formData.get('body') as string;
if (!title || !body) {
return { error: 'タイトルと本文は必須です' };
}
// 実際のアプリではここでDBに保存する
console.log('新しい記事:', { title, body });
return { success: true };
}第1引数に prevState を追加するのがポイントです。
今回は使いませんが、「前回のエラーをリセットする」といった処理で活用できます。
// app/posts/new/page.tsx
import CreatePostForm from "@/components/CreatePostForm";
export default function NewPostPage() {
return (
<main>
<CreatePostForm />
</main>
);
}useActionStateがよくわからなかった方は以下の記事を読んでみてください。
送信後にページを更新する — revalidatePath
Server Actionでデータを保存した後、ページに表示されているデータも最新の状態に更新したいことがあります。
そのために使うのが revalidatePath です。
// actions/post.ts
"use server";
import { revalidatePath } from 'next/cache';
type ActionResult = {
error?: string;
success?: boolean;
} | null;
export async function createPost(
prevState: ActionResult,
formData: FormData
): Promise<ActionResult> {
const title = formData.get('title') as string;
const body = formData.get('body') as string;
if (!title || !body) {
return { error: 'タイトルと本文は必須です' };
}
// DBへの保存処理(省略)
console.log('新しい記事:', { title, body });
// /posts ページのキャッシュを破棄して最新データを表示させる
revalidatePath('/posts');
return { success: true };
}revalidatePath('/posts') を呼ぶと、/posts ページのサーバー側キャッシュが「古い」とマークされます。
ブラウザがリロードされるわけではなく、次に誰かが /posts にアクセスしたときに初めてサーバーが最新データを取得してHTMLを再生成します。
revalidatePath('/posts') を呼んだ瞬間
→ /posts のキャッシュが「古い」とマークされる(ブラウザは何も起きない)
次に /posts にアクセスしたとき
→ キャッシュが古いのでサーバーが最新データを取得・再生成
→ 新しい内容が表示される#4で学んだ revalidate: 60 の手動版だと考えるとわかりやすいです。
| キャッシュを破棄するタイミング | |
|---|---|
next: { revalidate: 60 } | 60秒経過後、次のアクセス時 |
revalidatePath('/posts') | 呼び出した直後、次のアクセス時 |
実際の使用感としては、Server Actionの後に redirect('/posts') で一覧ページに飛ぶ流れが多いため、結果的に投稿直後に最新データが見えます。
「リロードされたように見える」のはそのためです。
送信後に別ページへリダイレクトする
投稿後に一覧ページへ飛ばしたい場合は redirect を使います。
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function createPost(
prevState: ActionResult,
formData: FormData
): Promise<ActionResult> {
const title = formData.get('title') as string;
const body = formData.get('body') as string;
if (!title || !body) {
return { error: 'タイトルと本文は必須です' };
}
// DBへの保存処理(省略)
revalidatePath('/posts');
redirect('/posts'); // 一覧ページへリダイレクト
}Route Handler vs Server Actions — どちらを使う?
#5と今回の記事で2種類のアプローチを学びました。
使い分けの基準を整理しておきましょう。
| 状況 | 使うべき方法 |
|---|---|
| Next.jsアプリ内のフォームからサーバー処理を呼ぶ | Server Actions |
| 外部サービス(モバイルアプリなど)からも使うAPIが必要 | Route Handler |
| ブラウザから直接URLでアクセスできるAPIが欲しい | Route Handler |
| ページのデータを更新・再取得したい | Server Actions + revalidatePath |
シンプルに言うと、Next.jsの中だけで完結するならServer Actions、外部から呼ばれる可能性があるならRoute Handler、と覚えておけば大抵の判断ができます。
ここまでのフォルダ構成
my-first-nextjs/
├── actions/ ← app/ と同列のルート直下
│ └── post.ts ← Server Actions("use server")
├── app/
│ └── posts/
│ └── new/
│ └── page.tsx ← フォームページ
└── components/
└── CreatePostForm.tsx ← Client Component(useActionState)まとめ
この記事で学んだこと:
- Server Actions は
"use server"を付けた非同期関数。APIエンドポイントを経由せずサーバー処理を呼べる <form action={serverAction}>で繋げるのが基本形useActionStateを使うと、成功・エラーのフィードバックや送信中の状態(isPending)を管理できるrevalidatePathでデータ更新後にページのキャッシュを破棄できるredirectで送信後に別ページへ飛ばせる- Route HandlerとServer Actionsは「外部からも呼ぶか」で使い分ける
次の記事ではTailwind CSSでスタイルを整え、メタデータAPIでSEO対策をする方法を学びます。ここまで機能優先で進めてきたサイトを、見た目も整った完成形に近づけましょう。


























