Next.js 16 の Server Actions【2026】ミューテーション・リバリデーション・面接質問

Next.js 16 の Server Actions がミューテーション、リバリデーション、pending 状態、楽観的 UI、セキュリティをどう扱うか、そして各概念を試す面接質問を解説します。

Next.js Server Actions のミューテーションとリバリデーションのフロー図

Next.js の Server Actions は、素朴な非同期関数をサーバーサイドのミューテーションエンドポイントへと変え、フォームから直接呼び出せるようにします。API ルートも、クライアント側の fetch も、手動の JSON シリアライズも不要です。Next.js 16 ではデータミューテーションを記述する標準的な手段となっており、2026 年のシニア React 面接ではほぼ確実に登場します。本記事では、Server Actions がどのように実行されるのか、リバリデーションがどのように最新データを UI へ伝播させるのか、pending 状態やエラー状態をどうモデル化するのか、そして多くの開発者がつまずくセキュリティのルールを詳しく掘り下げます。

一文での定義

Server Action とは、"use server" が付与された非同期関数であり、サーバー上でのみ実行されます。クライアントコンポーネントやフォームから、あたかもローカル関数であるかのように呼び出せる一方で、Next.js がネットワークリクエスト、引数のシリアライズ、そして CSRF 対策を自動的に処理します。

Server Actions は内部でどう実行されるのか

Server Action は、コンパイラが生成したエンドポイントを介してネットワーク越しに呼び出される、サーバー専用の関数です。関数に "use server" ディレクティブが付いている場合、Next.js のコンパイラはその本体をブラウザへ送信しません。代わりに、関数を軽量な参照へ置き換えます。ハッシュ化されたアクション ID を、生成された POST ルートに対応付けるのです。クライアントからアクションを呼び出すと、そのルートへリクエストが送られ、実際の関数はサーバー上で実行され、シリアライズされた結果が呼び出し元へストリーミングされて戻ります。

この設計には、プロダクションコードでも面接でも覚えておく価値のある帰結が三つあります。第一に、引数と戻り値はシリアライズ可能でなければなりません。エンコードされたペイロードとしてネットワークを越えるからです。第二に、関数本体がクライアント向けにバンドルされることはないため、シークレットはサーバー上に留まります。第三に、<form> に紐付けられたアクションはハイドレーション前でも動作します。これがプログレッシブエンハンスメントを中核的な利点とする理由です。実行モデルの全体像は、データ更新に関する Next.js の公式ガイド に記載されています。

すべての Server Action は、"use server" が付いたファイルまたはブロックの中に存在します。単一のミューテーションは次のようになります。

app/posts/actions.tstypescript
"use server"

import { revalidatePath } from "next/cache"
import { redirect } from "next/navigation"
import { PostService } from "@/lib/services/post-service"

export async function createPost(formData: FormData) {
  // FormData values arrive as strings; cast explicitly
  const title = formData.get("title") as string
  const body = formData.get("body") as string

  // Run business logic through the service layer, never the ORM directly
  const post = await PostService.create({ title, body })

  // Purge the cached list so the next request refetches it
  revalidatePath("/posts")

  // Send the user to the freshly created resource
  redirect(`/posts/${post.id}`)
}

この関数は送信された FormData を読み取り、サービス層を通じて永続化し、キャッシュされた一覧を無効化してリダイレクトします。これらのどれ一つとしてクライアント側の JavaScript を必要としません。

クライアント JavaScript なしで Server Action をフォームへ接続する

アクションをフォームの action プロパティに渡すのが最もシンプルな統合方法です。Next.js は送信内容を FormData へシリアライズし、サーバー上で関数を呼び出します。これはネイティブのフォーム送信の仕組みを使うため、JavaScript が無効でも機能し、ページがハイドレーションされるとシームレスにアップグレードされます。

app/posts/new-post-form.tsxtsx
import { createPost } from "./actions"

export default function NewPostForm() {
  return (
    <form action={createPost}>
      <input name="title" required />
      <textarea name="body" required />
      <button type="submit">Publish</button>
    </form>
  )
}

各フィールドの name 属性が、formData.get() で読み取られるキーになります。ブラウザがフォームからこのオブジェクトをどのように組み立てるかは、MDN の FormData リファレンス が説明しています。より細かな制御が必要な場合、next/form は同じアクションベースの契約を保ちながら、ネイティブ要素にクライアントサイドナビゲーションとプリフェッチを追加します。

useActionState で pending 状態とエラー状態を管理する

実運用のフォームには、バリデーションのフィードバックとローディングインジケーターが欠かせません。React 19 のフック useActionState は Server Action をラップし、各送信を通じて state 値を受け渡し、isPending フラグを公開します。これは以前の useFormState という名称に取って代わるものであり、現在の state、ラップされたアクション、そして pending を示す真偽値のタプルを返します。

app/posts/new-post-form.tsxtsx
"use client"

import { useActionState } from "react"
import { createPost } from "./actions"

const initialState = { error: "" }

export default function NewPostForm() {
  // state carries the last return value; isPending tracks the in-flight request
  const [state, formAction, isPending] = useActionState(createPost, initialState)

  return (
    <form action={formAction}>
      <input name="title" required />
      <textarea name="body" required />
      {state.error ? <p role="alert">{state.error}</p> : null}
      <button type="submit" disabled={isPending}>
        {isPending ? "Publishing..." : "Publish"}
      </button>
    </form>
  )
}

フックがアクションを駆動する場合、関数のシグネチャは FormData の前に prevState という第一引数を持つようになります。例外をスローする代わりにオブジェクトを返すことで、コンポーネントはエラーバウンダリなしでインラインバリデーションを描画できます。

app/posts/actions.tstypescript
"use server"

import { revalidateTag } from "next/cache"
import { PostService } from "@/lib/services/post-service"

type FormState = { error: string }

export async function createPost(
  prevState: FormState,
  formData: FormData,
): Promise<FormState> {
  const title = formData.get("title") as string

  // Validate on the server; client validation is never trustworthy alone
  if (!title || title.length < 3) {
    return { error: "Title must be at least 3 characters." }
  }

  await PostService.create({ title })

  // Invalidate by tag rather than path for finer-grained control
  revalidateTag("posts")
  return { error: "" }
}

react.dev の useActionState リファレンス では、ハイドレーション前の送信に対する permalink サポートといった高度なパターンも扱われています。構造化された state を返すことで、正常系とエラー系を予測可能な一箇所にまとめられます。

revalidatePath と revalidateTag:正しい無効化方法を選ぶ

ミューテーションは仕事の半分にすぎません。UI がその結果を反映しなければならないのです。Next.js は next/cache から二つの無効化関数を公開しており、正しい方を選ぶことは面接で頻出の判別ポイントになります。

関数無効化の対象適した場面
revalidatePath("/posts")あるルートに対するすべてのキャッシュエントリミューテーションが一つの明確なページまたはレイアウトに影響する場合
revalidateTag("posts")そのタグが付与されたすべてのキャッシュ済み fetch同じデータが複数のルートに現れる場合

revalidatePath は粗く、ルート中心です。URL とそのレイアウトツリーのキャッシュをクリアします。revalidateTag は細かく、データ中心です。その文字列でタグ付けされた fetch やキャッシュ済み関数は、どこに存在していても無効化されます。タグは大規模アプリでよりよくスケールします。一度のミューテーションで、サイドバー、一覧、そして詳細ページを単一の呼び出しで更新できるからです。どちらの関数もデータを即座に再取得するのではなく、古い(stale)とマークします。そのため、次のリクエストでコンテンツが再生成されます。

Next.js 16 の Cache Components では、タグ付けが cacheTag を通じて "use cache" ディレクティブと統合され、アプリ全体でキャッシュをどう捉えるかが変わります。この相互作用については、SharpSkill の Next.js 16 Cache Components ガイド で詳しく解説しています。

React / Next.jsの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

useOptimistic による楽観的 UI 更新

ネットワークの往復は、知覚できるほどのレイテンシを加えます。useOptimistic フックは期待される結果を即座に描画し、その後アクションが解決した時点でサーバーのレスポンスと整合させます。現在の state と、楽観的なバージョンを生成するリデューサーを受け取ります。

app/posts/like-button.tsxtsx
"use client"

import { useOptimistic } from "react"
import { likePost } from "./actions"

export function LikeButton({ postId, likes }: { postId: string; likes: number }) {
  // optimisticLikes updates before the server confirms
  const [optimisticLikes, addOptimisticLike] = useOptimistic(
    likes,
    (current) => current + 1,
  )

  async function handleLike() {
    addOptimisticLike(null) // update the UI immediately
    await likePost(postId)  // then run the real mutation
  }

  return (
    <form action={handleLike}>
      <button type="submit">Like ({optimisticLikes})</button>
    </form>
  )
}

アクションが失敗した場合、React は楽観的な値を確定済みのサーバー state へ自動的に戻すため、手動でのロールバックは不要です。react.dev の useOptimistic リファレンス では、整合サイクルがトランジションとどう相互作用するかが詳述されています。このパターンこそが、Server Action のフォームを、完全にクライアントレンダリングされた SPA と同じくらい反応の良いものに感じさせるものです。

引数のバインドとイベントハンドラからのアクション呼び出し

すべてのミューテーションがフォーム送信から始まるわけではありません。行の削除、設定の切り替え、リストの並べ替えは、多くの場合ボタンのクリックやその他のイベントから発火します。Server Action はクライアント上では通常の関数参照なので、任意のハンドラから呼び出せます。ただし、UI の応答性を保つために、その呼び出しはトランジション内で実行する必要があります。

フォームフィールドではない追加の引数は bind で付与します。これは、それらの値を先頭に付けた新しいアクションを生成します。送信された FormData と並べて ID を渡す際の慣用的な方法です。

app/posts/post-actions.tsxtsx
"use client"

import { useTransition } from "react"
import { deletePost, updatePost } from "./actions"

export function PostActions({ postId }: { postId: string }) {
  const [isPending, startTransition] = useTransition()

  // Bind the id so the action receives it before FormData
  const updateWithId = updatePost.bind(null, postId)

  return (
    <div>
      <form action={updateWithId}>
        <input name="title" />
        <button type="submit">Save</button>
      </form>

      {/* Event-handler invocation wrapped in a transition */}
      <button
        onClick={() => startTransition(() => deletePost(postId))}
        disabled={isPending}
      >
        {isPending ? "Deleting..." : "Delete"}
      </button>
    </div>
  )
}

バインドされた引数は、他のクローズオーバーされた値と同様に暗号化されるため、postId が転送中に改ざんされることはありません。とはいえ、アクションは呼び出し元がそのレコードを操作してよいかを依然として確認しなければなりません。直接の呼び出しを startTransition でラップすれば、React はインターフェースをインタラクティブに保ち、フォームのラッパーなしで pending 状態を表面化できます。

Server Actions のセキュリティ:すべてのアクションを公開 API として扱う

最も危険な誤解は、Server Action はサーバーコードの隣に書かれているからプライベートだ、というものです。実際には、アクションが一度使われると、Next.js は誰でも細工したリクエストで呼び出せる公開 POST エンドポイントを生成します。フレームワークは安全なアクション ID を提供し、next build の際に未使用のアクションを排除しますが、呼び出し元を認可することはありません。

すべてのアクションの内部で認可する

Server Actions は到達可能な HTTP エンドポイントです。レイアウトやミドルウェアでのセッションチェックはこれらを保護しません。攻撃者はアクションへ直接 POST できるからです。データに触れる前に、アクション本体の中でセッションと呼び出し元の権限を検証してください。

app/admin/actions.tstypescript
"use server"

import { auth } from "@/lib/auth"
import { headers } from "next/headers"
import { UserService } from "@/lib/services/user-service"

export async function deleteUser(userId: string) {
  // Authenticate on every invocation, not in a wrapper component
  const session = await auth.api.getSession({ headers: await headers() })
  if (!session || session.user.role !== "admin") {
    throw new Error("Unauthorized")
  }

  await UserService.delete(userId)
}

さらに二つのルールが重要です。インラインアクションによってクローズオーバーされた値は、クライアントへ送信される前に暗号化され、サーバー上で復号されます。そのためキャプチャされたトークンが平文で漏れることはありませんが、それに依存するのは脆弱です。シークレットはクロージャから完全に排除してください。そして、クライアントから来る引数はすべて信頼できない入力であり、REST のボディとまったく同じように検証しなければなりません。Next.js のデータセキュリティガイド は、これらの保証の背後にある taint モデルを定式化しています。これらのトレードオフを体系的に練習するには、SharpSkill の Next.js Server Actions 面接モジュール が、採用パネルが尋ねるまさにその質問を鍛えてくれます。

Next.js 16 Server Actions の面接質問

これらの質問は、2026 年のシニア React およびフルスタックの面接ループで繰り返し登場します。

なぜ Server Action はシリアライズ可能な値しか受け取れず、返せないのか。 引数と結果が、エンコードされたペイロードとしてクライアントとサーバーの間のネットワーク境界を越えるからです。関数、クラスインスタンス、DOM ノードはシリアライズできないため、それらを渡すと例外がスローされます。FormData、プレーンオブジェクト、配列、プリミティブは安全です。

Server Actions ではプログレッシブエンハンスメントはどう機能するのか。 アクションをフォームの action プロパティへ直接渡すと、ブラウザは React のハイドレーション前でも POST リクエストでフォームをネイティブに送信します。Next.js がこれをインターセプトしてサーバーサイドでアクションを実行するため、フォームはクライアント JavaScript なしで機能し、ハイドレーション後にはクライアント駆動の体験へアップグレードされます。

アクションでエラーをスローすることと返すことの違いは何か。 スローは最も近いエラーバウンダリへ伝播し、予期しない失敗に適しています。useActionState を通じて構造化された state オブジェクトを返すのは、フォームをアンマウントせずにインラインで描画されるべき、想定内のバリデーションエラーに適しています。

revalidatePath を呼ぶとデータは即座に再取得されるのか。 いいえ。キャッシュエントリを古い(stale)とマークし、次のリクエストでそれらを再生成させます。現在のレスポンスは影響を受けません。だからこそ、ミューテーション後のリダイレクトは新しく描画されたコンテンツに着地します。

これらのミューテーションが実行される、より広範なレンダリングの文脈については、プロダクションにおける React 19 Server Components の解説が、Server Actions の知識と自然に組み合わさります。

React / Next.jsの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

まとめ

Next.js 16 の Server Actions は、ミューテーション、リバリデーション、そしてプログレッシブエンハンスメントを、単一のサーバーファーストなプリミティブへと統合します。要点は次のとおりです。

  • ミューテーションには "use server" を付け、フォームの action プロパティへ渡すことで、ネットワーク処理とプログレッシブエンハンスメントを無償で得られます。
  • pending とバリデーションの state は useActionState で駆動し、想定内のエラーではスローせずに構造化オブジェクトを返します。
  • 変更が一つのルートに及ぶときは revalidatePath を、同じデータが複数のルートにまたがるときは revalidateTag を選びます。
  • useOptimistic を使って期待される結果を即座に描画し、React に整合またはロールバックを自動で任せます。
  • 各アクションはミドルウェアだけでは保護されない公開 HTTP エンドポイントなので、すべてのアクション本体の内部で認可と検証を行います。

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

今日のチャレンジ

React / Next.js のバグを見つけられますか

実際のコード、隠れたバグ、1日1回。アカウントなしで試せます。

Anthony Fillion-Maillet

執筆

Anthony Fillion-Maillet

SharpSkill 創業者

10 年以上フルスタック開発に携わっています。SharpSkill を運営し、ここで公開される内容に責任を負っています。

2026年7月6日 更新

タグ

#next.js
#server-actions
#react
#mutations
#revalidation
#interview

共有

関連記事