Skip to content

5.4 Server Actions とフォーム処理 ​

5.3 までは、 Server Component が データを取って画面を作る ところを見てきました。この章の題材は逆向きで、 ユーザーの入力を受けてサーバーを更新する 流れのほうです。React 19 と Next.js App Router は、 これを「フォームに関数を直接渡す」 スタイルで書けるようにしています。それが Server Functions / Server Actions と呼ばれる仕組みです。

Part 4 で扱った useActionState と直接つながる話でもあります。Part 4 では Client 側の「Actions」 を見ましたが、 その action がサーバーで実行される場合の話が、 ここで扱う内容になります。

5.4.1 用語の整理:Server Function と Server Action ​

最初に用語を揃えておきましょう。似た言葉が何度か出てきます。

  • Server Function(React の用語): 'use server' ディレクティブが付いた、 サーバーで実行される async 関数のこと。
  • Server Action(Next.js や React でよく出る用語): Server Function のうち、 <form action={...}> や useActionState など「アクション」 の文脈で使うもの。

react.dev の Server Functions ページからの引用です。

If a Server Function is passed to an action prop or called from inside an action then it is a Server Action, but not all Server Functions are Server Actions. (react.dev / Server Functions)

Next.js の「Mutating Data」 ページも、 同じ線引きをしています。

A Server Action is a Server Function used in a specific way (for handling form submissions and mutations). Server Function is the broader term. (nextjs.org / Mutating Data)

実用上は ほぼ同じ意味 で登場することが多いのですが、「フォームで使えば Server Action、 そうでなくても Server Function」 という整理でだいたい通ります。

5.4.2 'use server' の置き方 ​

Server Function は 'use server' ディレクティブで作ります。これは 5.2 で触れた通り、 Server Component のためのディレクティブではありません。Server Function のためのものです。

置き方は 2 通りあります。

ファイル先頭に置く ​

ファイル全体をまとめて「このファイルが export する async 関数は Server Function」 とする書き方です。Client Component から import する場合は、 この形式にします。

ts
// app/lib/actions.ts
'use server';

export async function createPost(formData: FormData) {
  const title = formData.get('title');
  const content = formData.get('content');
  // DB 書き込みなど
}

関数本体の先頭に置く ​

Server Component の中でインラインに書くときは、 関数本体の先頭に置きます。

tsx
// app/page.tsx
export default function Page() {
  async function createPost(formData: FormData) {
    'use server';
    // ...
  }

  return (
    <form action={createPost}>
      <input name="title" />
      <button type="submit">投稿</button>
    </form>
  );
}

公式の制約も引用しておきます。

'use server' must be at the very beginning of their function or module; above any other code including imports (comments above directives are OK). They must be written with single or double quotes, not backticks. (react.dev / 'use server')

  • 対象は async 関数のみ
  • 引数と戻り値は serializable でなくてはいけない(関数や class instance は渡せません)

5.4.3 <form action={...}> で呼ぶ ​

もっとも基本の呼び出し方は、 HTML の <form> の action 属性に Server Function を渡す形です。

ts
// app/actions.ts
'use server';

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  // DB 書き込み
}
tsx
// app/ui/form.tsx
import { createPost } from '@/app/actions';

export function PostForm() {
  return (
    <form action={createPost}>
      <input name="title" />
      <button type="submit">投稿</button>
    </form>
  );
}

ブラウザ側から見ると、 これは送信時に サーバーの createPost を呼び出すリクエスト に変換されます。Next.js の記述を引いておきます。

Behind the scenes, actions use the POST method, and only this HTTP method can invoke them. (nextjs.org / Mutating Data)

つまり Server Action は、 内部的には POST エンドポイントです。フォームに name 属性を付けた <input> を並べれば、 FormData としてそれが関数に渡ってきます。

図 5.4.1 Server Action のリクエストフロー(シーケンス) ​

ここで大事なのは、 1 回のラウンドトリップで「アクションの実行」 と「画面の更新」 が両方返ってくる ところです。SPA なら POST /api/posts → 成功したら GET /api/posts で一覧を取り直す、 という 2 回の往復にしていたものが、 1 回で終わります。

図 5.4.2 form action → Server Action → revalidate の全体像 ​

もう少し俯瞰的に、 「フォームから DB 更新まで」 と「そのあとキャッシュを更新して新しい HTML を返すまで」 を 1 枚の絵で見てみます。

ブラウザNext.js サーバーDB / キャッシュ① フォーム送信<form action={fn}>POST② Server Function を実行'use server' 関数 / 認証・バリデーション③ DB 更新INSERT / UPDATE / DELETE④ revalidatePath / Tag でキャッシュ無効化古い Server Component の結果を捨てる⑤ キャッシュ破棄該当ページが再生成対象⑥ 新しい RSC Payload を生成再レンダー結果 + Client Component 参照⑦ 画面が更新1 回のラウンドトリップ・SPA は「POST してから GET で取り直し」 の 2 往復。ここでは 1 往復で終わります・`revalidatePath` は「関連するページを次回再生成」 の指示。即座に再描画されるわけではありません・`redirect` を同時に呼ぶと、⑦ は遷移先の画面になります

5.4.4 useActionState との組み合わせ ​

フォームには「送信中の表示」 や「バリデーションエラーメッセージ」 が必要になります。これを扱うのが、 React 19 の useActionState です。Part 4 の 4.2 Actions / use でも登場しています。

シグネチャは次の通りです(react.dev より)。

ts
const [state, dispatchAction, isPending] = useActionState(
  reducerAction,
  initialState,
  permalink?
);

第 2 要素の dispatchAction は呼び出し側で任意の名前を付けられるのですが、 以降の実装例では <form action={...}> に渡すことを強調するために formAction で統一しておきます(react.dev のシグネチャでは dispatchAction と呼ばれている同じものです)。

Server Function と組み合わせると、 フォーム送信の pending とエラー状態を簡潔に扱えます。

ts
// app/actions.ts
'use server';

export type PostState = { error?: string; ok?: boolean };

export async function createPost(
  _prev: PostState,
  formData: FormData
): Promise<PostState> {
  const title = (formData.get('title') ?? '').toString().trim();
  if (title.length === 0) {
    return { error: 'タイトルを入力してください' };
  }
  // DB 書き込み
  return { ok: true };
}
tsx
// app/ui/post-form.tsx
'use client';

import { useActionState } from 'react';
import { createPost, type PostState } from '@/app/actions';

const initial: PostState = {};

export default function PostForm() {
  const [state, formAction, isPending] = useActionState(createPost, initial);

  return (
    <form action={formAction}>
      <input name="title" />
      <button type="submit" disabled={isPending}>
        {isPending ? '送信中...' : '投稿'}
      </button>
      {state.error && <p role="alert">{state.error}</p>}
      {state.ok && <p>投稿しました</p>}
    </form>
  );
}

formAction を <form action={...}> に渡すと、 React がフォーム送信を自動で transition にラップしてくれて、 isPending が自動で立ちます。

5.4.5 バリデーションとセキュリティ ​

Server Action を書くときに 必ず意識すべき点 を、 Next.js のドキュメントから直接引用しておきます。

Server Functions are reachable via direct POST requests, not just through your application's UI. Always verify authentication and authorization inside every Server Function. (nextjs.org / Mutating Data)

Server Action は <form action={...}> 経由でだけ呼ばれるわけではありません。 クライアントから任意の引数で POST されうる エンドポイントだということです。つまり以下は Server Function の関数本体で毎回やる 必要があります。

  1. 認証チェック: そのユーザーがログインしているか
  2. 認可チェック: そのユーザーがそのリソースを操作してよいか
  3. 入力バリデーション: 期待する型・長さ・範囲に収まっているか

バリデーションには Zod のようなスキーマライブラリを使うのが一般的です。

ts
// app/actions.ts
'use server';

import { z } from 'zod';
import { auth } from '@/lib/auth';

const CreatePost = z.object({
  title: z.string().min(1).max(200),
  content: z.string().min(1).max(10000),
});

export async function createPost(_prev: unknown, formData: FormData) {
  const session = await auth();
  if (!session?.user) return { error: 'Unauthorized' };

  const parsed = CreatePost.safeParse({
    title: formData.get('title'),
    content: formData.get('content'),
  });
  if (!parsed.success) return { error: '入力が不正です' };

  // DB 書き込み
  return { ok: true };
}

NOTE: unverified — 要確認: Zod は案件で最もよく使われる選択肢のひとつですが、 Valibot, Yup などの代替もあります。本サイトでは深入りしません。

5.4.6 キャッシュの更新(revalidatePath / revalidateTag) ​

Next.js は、 Server Component のレンダリング結果やデータフェッチの結果をキャッシュします。Server Action で DB を書き換えた直後、 一覧ページのキャッシュが古いままだと、 ユーザーには更新が反映されていないように見えてしまいます。

これを解決するのが revalidatePath と revalidateTag です。

ts
// app/actions.ts
'use server';

import { revalidatePath } from 'next/cache';

export async function createPost(formData: FormData) {
  // DB 書き込み
  revalidatePath('/posts'); // /posts ページのキャッシュを無効化
}

公式の説明は次の通りです。

revalidatePath allows you to invalidate cached data on-demand for a specific path. (nextjs.org / revalidatePath)

revalidateTag のほうは、 fetch に付けたタグに紐づく複数のキャッシュをまとめて無効化します。「このデータを使っているページ全部を更新したい」 ときに便利です。

Server Action 内でリダイレクトも一緒に行えます。

ts
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';

export async function createPost(formData: FormData) {
  // DB 書き込み
  revalidatePath('/posts');
  redirect('/posts');
}

redirect は制御フロー用の例外を投げて実行を中断します。redirect の後ろのコードは走らないので、 revalidatePath は redirect より前 に書きます。

5.4.7 まとめ:フォーム処理の頭の整理 ​

RSC + Next.js でフォームを書くときに、 何をどこに書くかを 1 枚にすると次の形になります。

やること置き場所
フォーム UIServer Component でも Client Component でも OK
<form action={...}> で渡す関数Server Function ('use server')
認証・認可・バリデーションServer Function の関数本体(毎回やる)
pending / エラー状態Client Component + useActionState
キャッシュ更新revalidatePath / revalidateTag を Server Function 内で呼ぶ
画面遷移Server Function 内で redirect を呼ぶ

「フロント側で fetch を書かない」「 API ルートを自分で作らない」 というのが、 App Router スタイルのフォーム処理の特徴です。その代わり、 Server Function は常にクライアントから直接呼ばれうるエンドポイントである という意識は、 忘れないようにしておいてください。

参照ソース ​