言語を切り替える
テーマを切り替える

Next.js Server Actions チュートリアル:フォーム処理とバリデーションのベストプラクティス

PC の前で、ユーザー登録フォームのコードを見ています。フォルダにはすでに 4 つのファイル——フォームコンポーネント、API Route、型定義、エラー処理——が積み重なっています。シンプルなフォーム送信のために、200 行近いコードを書いたことになります。

もっとシンプルな方法はないのでしょうか。

答えは Server Actions です。Next.js App Router のこの機能なら、フォーム処理フローを 80% 簡素化できます。API Route を書かず、手動 fetch も不要。煩雑な状態管理すらいりません。便利そうですが、「本当に安全なのか」「バリデーションはどうする」「Loading 状態はどう処理する」といった疑問も湧くでしょう。

使い始めの頃、私も同じ不安がありました。数ヶ月使い込み、いくつかの落とし穴を踏んだうえで得た知見を共有します。Next.js Server Actions を使ったフォーム処理の実戦テクニック——基本の送信から Zod バリデーション、セキュリティ対策、UX 最適化まで、この記事では実際のコード例で素早く習得できます。

Server Actions の基礎

Server Actions とは

Server Actions はサーバーで実行される非同期関数です。'use server' でマークすれば、フォームの action 属性に直接渡せます。フォーム送信時に自動で呼び出され、データ処理、DB 操作、キャッシュ更新——すべてサーバー側で完結します。

主な特徴:

  • 型安全:TypeScript が全体のチェーンを検証
  • ゼロ設定/api フォルダを作る必要なし
  • 自動処理:FormData が自動で渡される

書き方は 2 通り。Server Action をコンポーネント内に直接書く(インライン)か、別ファイルに置く(モジュールレベル)かです:

// 方式1:コンポーネント内にインライン
export default function Page() {
  async function createUser(formData: FormData) {
    'use server' // Server Action としてマーク
    const name = formData.get('name')
    // データ処理...
  }

  return <form action={createUser}>...</form>
}

// 方式2:独立ファイル(推奨)
// app/actions.ts
'use server' // ファイルレベルマーク

export async function createUser(formData: FormData) {
  const name = formData.get('name')
  // データ処理...
}

Server Actions と従来の API Routes の違いは?いつどちらを使うべきか?

比較表を整理しました:

特性Server ActionsAPI Routes
用途フォーム送信、データ変更RESTful API、外部呼び出し
HTTP メソッドPOST のみGET/POST/PUT/DELETE など
型安全ネイティブに型安全手動で型定義が必要
呼び出し方関数を直接呼び出しfetch リクエスト
適した場面内部ロジック、フォーム公開 API、サードパーティ連携
コード量少ない相対的に多い

シンプルに言えば:内部は Server Actions、外部は API Routes。自分のアプリ内のフォーム処理なら Server Actions で十分。他システム向け API や GET リクエストが必要なら API Routes を使いましょう。

Vercel 2025 年の調査によると、すでに 63% の開発者が本番環境で Server Actions を使っています。実験的機能ではありません。

"63% の開発者が本番環境で Server Actions を使用"

最初の Server Actions 例

コードで見てみましょう。最もシンプルなログインフォームです:

// app/login/page.tsx
export default function LoginPage() {
  async function handleLogin(formData: FormData) {
    'use server' // サーバー関数としてマーク

    // フォームからデータ取得
    const email = formData.get('email') as string
    const password = formData.get('password') as string

    // ログイン処理(ここでは簡略化)
    console.log('ログイン試行:', email)

    // 実プロジェクトではユーザー検証、token 生成など
  }

  return (
    <form action={handleLogin}>
      <input
        type="email"
        name="email"
        placeholder="メールアドレス"
        required
      />
      <input
        type="password"
        name="password"
        placeholder="パスワード"
        required
      />
      <button type="submit">ログイン</button>
    </form>
  )
}

これだけです。ポイント:

  1. 'use server':Next.js にこの関数をサーバーで実行させる
  2. formData.get():フィールドの name 属性で値を取得
  3. action={handleLogin}:フォーム送信時に自動呼び出し

動作:送信ボタンをクリックすると、ページはリロードされず、データがサーバーに直接送られます。従来方式より fetchuseState、エラー処理のコードが大幅に減ります。

ただしこれは最基礎。実プロジェクトではバリデーション、エラー表示、Loading 状態の処理が必要です。続きを見ていきましょう。

フォームバリデーション実践

Zod でフォームバリデーション

クライアントの required 属性だけ?甘すぎます。ユーザーはブラウザの開発者ツールで簡単にバイパスできます。サーバー側バリデーションは必須です。

ここで Zod が活躍します。サーバー側でデータ形式を検証し、問題があれば即座にエラーを返し、不正データの DB 流入を防ぎます。

まず Zod をインストール:

npm install zod

バリデーションルールを定義:

// app/actions.ts
'use server'

import { z } from 'zod'

// バリデーション schema
const SignupSchema = z.object({
  name: z.string().min(2, '名前は 2 文字以上'),
  email: z.string().email('メール形式が正しくありません'),
  password: z.string().min(8, 'パスワードは 8 文字以上'),
})

export async function signup(formData: FormData) {
  // FormData からデータ抽出
  const rawData = {
    name: formData.get('name'),
    email: formData.get('email'),
    password: formData.get('password'),
  }

  // データ検証
  const result = SignupSchema.safeParse(rawData)

  // 検証失敗時はエラーを返す
  if (!result.success) {
    return {
      success: false,
      errors: result.error.flatten().fieldErrors, // フィールド単位のエラー
    }
  }

  // 検証通過、ビジネスロジックを処理
  const { name, email, password } = result.data

  // ユーザー作成、DB 保存など...
  console.log('ユーザー作成:', { name, email })

  return {
    success: true,
    message: '登録成功!',
  }
}

ポイント:

  1. safeParse は例外を投げない:失敗時 { success: false, error: ... } を返し、エレガントに処理可能
  2. flatten().fieldErrors:検証エラーを { name: ['エラー1'], email: ['エラー2'] } 形式に変換、表示しやすい
  3. 構造化データを返すsuccess フラグとエラー情報を含み、クライアントが表示方法を決定

ただし、これらのエラーをフォームにどう表示するか。ここで useActionState が必要になります。

バリデーションエラーの表示:useActionState

useActionState は React 19 で導入された Hook(以前は useFormState)で、Server Actions の返却状態を処理するために設計されています。機能:

  • サーバー返却データをコンポーネント状態に保存
  • ラップされた action 関数を提供
  • フォームが送信中かどうかを通知

コードを見てみましょう:

// app/signup/page.tsx
'use client' // Hook 使用にはクライアントコンポーネント

import { useActionState } from 'react'
import { signup } from '@/app/actions'

export default function SignupPage() {
  // 初期状態を定義
  const initialState = { success: false, errors: {}, message: '' }

  // useActionState:Server Action と初期状態を受け取る
  const [state, formAction, isPending] = useActionState(signup, initialState)

  return (
    <form action={formAction}> {/* 元の action の代わりに formAction */}
      <div>
        <label>名前</label>
        <input
          type="text"
          name="name"
          required
        />
        {/* フィールドエラーを表示 */}
        {state.errors?.name && (
          <p className="error">{state.errors.name[0]}</p>
        )}
      </div>

      <div>
        <label>メール</label>
        <input
          type="email"
          name="email"
          required
        />
        {state.errors?.email && (
          <p className="error">{state.errors.email[0]}</p>
        )}
      </div>

      <div>
        <label>パスワード</label>
        <input
          type="password"
          name="password"
          required
        />
        {state.errors?.password && (
          <p className="error">{state.errors.password[0]}</p>
        )}
      </div>

      <button type="submit" disabled={isPending}>
        {isPending ? '送信中...' : '登録'}
      </button>

      {/* 成功メッセージを表示 */}
      {state.success && (
        <p className="success">{state.message}</p>
      )}
    </form>
  )
}

フロー:

  1. ユーザーがフォーム送信 → signup を呼び出し
  2. サーバー側で検証失敗 → { success: false, errors: {...} } を返却
  3. useActionState が結果を state に保存
  4. コンポーネント再レンダリング、エラー情報を表示

isPending はフォーム送信中 true、完了後 false。ボタンの無効化や Loading テキスト表示に使えます。

気づいたかもしれませんが、検証失敗後にユーザー入力が消えます。入力を保持するには、返却時に values フィールドを追加し、入力欄に defaultValue を設定します。ここでは省略しますが、useActionState の役割を理解することが重要です:クライアントコンポーネントと Server Actions を接続し、状態管理をシンプルにする

ユーザー体験の最適化

Loading 状態と二重送信防止

上記では isPending で Loading を表示しましたが、もう 1 つの Hook があります:useFormStatus。この 2 つは混同しやすく、最初は私も戸惑いました。

シンプルに言えば:

  • useActionStateisPending:フォームコンポーネント内で使う
  • useFormStatuspending:フォームの子コンポーネント(送信ボタンなど)内で使う

useFormStatus には制限があります:<form> の子コンポーネント内でのみ呼び出せ、フォームコンポーネント内では直接使えません。一見面倒ですが、ボタンを独立コンポーネントに切り出して再利用できるメリットがあります。

例:送信ボタンを分離

// components/SubmitButton.tsx
'use client'

import { useFormStatus } from 'react-dom'

export function SubmitButton({ children }: { children: React.ReactNode }) {
  const { pending } = useFormStatus() // フォーム送信状態を取得

  return (
    <button
      type="submit"
      disabled={pending}
      className={pending ? 'loading' : ''}
    >
      {pending ? '送信中...' : children}
    </button>
  )
}

フォーム内で使用:

// app/signup/page.tsx
'use client'

import { useActionState } from 'react'
import { signup } from '@/app/actions'
import { SubmitButton } from '@/components/SubmitButton'

export default function SignupPage() {
  const [state, formAction] = useActionState(signup, { success: false, errors: {} })

  return (
    <form action={formAction}>
      {/* フォームフィールド... */}

      <SubmitButton>登録</SubmitButton> {/* Loading を自動処理 */}

      {state.errors?.general && (
        <p className="error">{state.errors.general}</p>
      )}
    </form>
  )
}

ボタンの Loading ロジックが完全にカプセル化されます。送信中:

  • ボタンが自動で無効化、二重送信を防止
  • テキストが「送信中…」に変化
  • スピナーアニメーションも追加可能

pendingisPending の違い?

特性useActionStateisPendinguseFormStatuspending
呼び出し位置フォームコンポーネント内部フォームの子コンポーネント内部
適した場面フォーム全体の状態にアクセスが必要送信状態のみ必要な独立ボタン
柔軟性state と pending を同時取得pending のみ取得

実プロジェクトでは、こう使い分けています:

  • フォームロジックが複雑、複数状態を処理 → useActionState
  • 汎用送信ボタンを作る → useFormStatus

プログレッシブエンハンスメント

Server Actions にはプログレッシブエンハンスメントという機能があります。JavaScript が無効でもフォーム送信が動作します。

Server Actions は本質的にブラウザネイティブの <form> 送信を利用しています。JavaScript がある場合、Next.js が送信を横取りして AJAX リクエストに。JavaScript がない場合は従来のフォーム送信にフォールバックします。

実用シーンは正直、多くありません。今どき JavaScript なしで使えるサイトは少ない……。ただしアクセシビリティやクローラー対応の観点では加点要素。何もしなくても Next.js が自動処理します。

セキュリティとベストプラクティス

Server Actions のセキュリティ

最も見落とされがちな部分です。Server Actions がサーバーで動くから自動的に安全——大間違い

Server Actions は本質的に公開 API エンドポイントです。Next.js が推測しにくい ID を生成しますが、これは「難読化」であって真のセキュリティ対策ではありません。技術に詳しい人なら、ブラウザの開発者ツールでネットワークリクエストを見れば Action ID を見つけ、手動で呼び出せます。

Next.js が提供する組み込み保護:

  1. CSRF 対策:Server Actions は POST のみ呼び出し可能。Origin と Host ヘッダーの一致を確認。クロスサイトリクエストは拒否
  2. 安全な Action ID:各 Action に暗号化 ID。総当たり攻撃が困難
  3. クロージャ変数の暗号化:Action 内で外部変数を使う場合、Next.js が暗号化

これだけでは不十分。必ず以下を実施

1. 入力検証

クライアントデータを決して信頼しない。前述の Zod バリデーションは必須です。

2. 認証

ユーザーがログインしているか確認。権限が必要な Action すべてで認証を検証。

3. 認可

ログイン ≠ 権限あり。例:ユーザー A がユーザー B のデータを削除できない。操作権限を検証。

実例:

// app/actions.ts
'use server'

import { cookies } from 'next/headers'
import { z } from 'zod'

const DeletePostSchema = z.object({
  postId: z.string().min(1),
})

export async function deletePost(formData: FormData) {
  // 1. 入力検証
  const rawData = {
    postId: formData.get('postId'),
  }

  const result = DeletePostSchema.safeParse(rawData)
  if (!result.success) {
    return { success: false, error: '無効なリクエスト' }
  }

  const { postId } = result.data

  // 2. 認証:ログイン確認
  const cookieStore = await cookies()
  const sessionToken = cookieStore.get('session')?.value

  if (!sessionToken) {
    return { success: false, error: '先にログインしてください' }
  }

  // 3. 現在のユーザーを取得
  const currentUser = await getUserFromSession(sessionToken)
  if (!currentUser) {
    return { success: false, error: 'セッションが期限切れです' }
  }

  // 4. 認可:記事が現在のユーザー所有か確認
  const post = await getPost(postId)
  if (!post) {
    return { success: false, error: '記事が存在しません' }
  }

  if (post.authorId !== currentUser.id) {
    return { success: false, error: 'この記事を削除する権限がありません' }
  }

  // 5. 操作実行
  await deletePostFromDB(postId)

  return { success: true, message: '削除成功' }
}

この例は完全なセキュリティチェックフローを示しています:入力検証 → 認証 → 認可 → 操作実行。どれも欠かせません。

おすすめツール:next-safe-action ライブラリ。ミドルウェア機構でバリデーション、認証、エラー処理を統一:

import { createSafeActionClient } from 'next-safe-action'

// 認証付き action クライアントを作成
const actionClient = createSafeActionClient({
  // ミドルウェア:ログイン状態を確認
  async middleware() {
    const session = await getSession()
    if (!session) {
      throw new Error('未ログイン')
    }
    return { userId: session.userId }
  },
})

// 使用時に認証チェックが自動適用
export const deletePost = actionClient
  .schema(DeletePostSchema)
  .action(async ({ parsedInput, ctx }) => {
    const { postId } = parsedInput
    const { userId } = ctx // ミドルウェアからユーザー ID を取得

    // 削除実行...
  })

認証が必要な Action すべてで同じロジックを再利用でき、コードがすっきりします。

覚えておいてください:Server Actions は魔法ではなく、API エンドポイントです。必要なセキュリティ対策はすべて実施してください。

実戦例:認証付きフォーム

完全な例——ログインユーザーのみが送信できるコメントフォーム:

// app/actions.ts
'use server'

import { cookies } from 'next/headers'
import { z } from 'zod'
import { revalidatePath } from 'next/cache'

const CommentSchema = z.object({
  postId: z.string(),
  content: z.string().min(1, 'コメントは空にできません').max(500, 'コメントは 500 文字以内'),
})

export async function addComment(formData: FormData) {
  // 1. 入力検証
  const rawData = {
    postId: formData.get('postId'),
    content: formData.get('content'),
  }

  const result = CommentSchema.safeParse(rawData)
  if (!result.success) {
    return {
      success: false,
      errors: result.error.flatten().fieldErrors,
    }
  }

  // 2. 認証
  const cookieStore = await cookies()
  const sessionToken = cookieStore.get('session')?.value

  if (!sessionToken) {
    return {
      success: false,
      error: 'コメントするには先にログインしてください',
    }
  }

  const user = await getUserFromSession(sessionToken)
  if (!user) {
    return {
      success: false,
      error: 'セッションが期限切れです。再ログインしてください',
    }
  }

  // 3. コメント保存
  const { postId, content } = result.data

  await saveComment({
    postId,
    content,
    authorId: user.id,
    authorName: user.name,
    createdAt: new Date(),
  })

  // 4. ページキャッシュを再検証、コメントを即座に表示
  revalidatePath(`/posts/${postId}`)

  return {
    success: true,
    message: 'コメント成功',
  }
}

クライアントコンポーネント:

// app/posts/[id]/CommentForm.tsx
'use client'

import { useActionState } from 'react'
import { addComment } from '@/app/actions'
import { SubmitButton } from '@/components/SubmitButton'

export function CommentForm({ postId }: { postId: string }) {
  const [state, formAction] = useActionState(addComment, {
    success: false,
    errors: {},
  })

  return (
    <form action={formAction}>
      {/* 非表示フィールドで postId を渡す */}
      <input type="hidden" name="postId" value={postId} />

      <textarea
        name="content"
        placeholder="コメントを書く..."
        rows={4}
        required
      />

      {state.errors?.content && (
        <p className="error">{state.errors.content[0]}</p>
      )}

      {state.error && (
        <p className="error">{state.error}</p>
      )}

      {state.success && (
        <p className="success">{state.message}</p>
      )}

      <SubmitButton>コメントを投稿</SubmitButton>
    </form>
  )
}

この例は前述のすべてのポイントを組み合わせています:

  • Zod 入力検証
  • ログイン状態確認
  • useActionState で状態管理
  • revalidatePath でキャッシュ更新
  • Loading 状態付き送信ボタン

本番で使える完全なフォーム処理フローです。

応用テクニック

追加パラメータの渡し方

フォームフィールド以外のパラメータを渡す必要がある場合があります。記事編集時にフォーム内容に加えて記事 ID を渡す、など。

1 つの方法は非表示フィールド:

<input type="hidden" name="postId" value={postId} />

よりエレガントな方法:JavaScript の bind メソッド。

// app/actions.ts
'use server'

export async function updatePost(postId: string, formData: FormData) {
  const title = formData.get('title') as string
  const content = formData.get('content') as string

  // 記事更新...
  await updatePostInDB(postId, { title, content })

  return { success: true }
}

クライアント側:

// app/posts/[id]/edit/page.tsx
'use client'

import { updatePost } from '@/app/actions'

export default function EditPost({ postId }: { postId: string }) {
  // bind で postId パラメータを固定
  const updatePostWithId = updatePost.bind(null, postId)

  return (
    <form action={updatePostWithId}>
      <input type="text" name="title" required />
      <textarea name="content" required />
      <button type="submit">更新</button>
    </form>
  )
}

bind(null, postId) は新しい関数を作成し、postId を第 1 引数として固定します。フォーム送信時、FormData が第 2 引数として渡されます。

編集、削除など ID が必要な操作に適しています。

データ再検証

Server Actions でデータ処理後、関連ページのキャッシュが古くなっている可能性があります。Next.js は 2 つの関数でキャッシュを更新できます:

1. revalidatePath

パス単位で更新:

import { revalidatePath } from 'next/cache'

export async function createPost(formData: FormData) {
  // 記事作成...

  // トップページの記事一覧を更新
  revalidatePath('/')
  // 記事詳細ページを更新
  revalidatePath(`/posts/${newPostId}`)

  return { success: true }
}

2. revalidateTag

タグ単位で更新(fetch 時にタグ付けが必要):

// データ取得時にタグ付け
fetch('https://api.example.com/posts', {
  next: { tags: ['posts'] }
})

// Server Action 内で 'posts' タグのキャッシュをすべて更新
import { revalidateTag } from 'next/cache'

export async function createPost(formData: FormData) {
  // 記事作成...

  revalidateTag('posts') // 関連キャッシュをすべて更新

  return { success: true }
}

いつどちらを使う?

  • パスが固定で数が少ないrevalidatePath
  • データが複数ページに分散revalidateTag

私は通常 revalidatePath を優先します。シンプルで直接的。1 つの操作が多数のページに影響する場合のみタグを検討します。

楽観的更新

ほぼ失敗しない操作——いいね、お気に入りなど——には楽観的更新が有効です。UI 上で先に成功を表示し、バックグラウンドで送信します。

React 19 は useOptimistic Hook を提供:

'use client'

import { useOptimistic } from 'react'
import { likePost } from '@/app/actions'

export function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
  const [optimisticLikes, setOptimisticLikes] = useOptimistic(initialLikes)

  async function handleLike() {
    // UI を即座に更新(楽観的)
    setOptimisticLikes(optimisticLikes + 1)

    // バックグラウンドで送信
    await likePost(postId)
  }

  return (
    <button onClick={handleLike}>
      👍 {optimisticLikes}
    </button>
  )
}

ボタンクリックで数字が即座に +1。サーバー応答を待つ必要なし。体験が滑らか。

ただし、成功率が非常に高い操作にのみ使ってください。失敗時の UI ロールバックが面倒になります。

まとめ

3 点に絞ります:

  1. Server Actions はフォーム処理を簡素化しますが、万能ではありません。内部フォームには使い、外部 API には Route Handlers。すべて Server Actions に統一しないでください。

  2. セキュリティは自分で担保。フレームワークが提供するのは基本対策のみ。入力検証、認証、認可チェック——必要なものはすべて実施。Next.js にすべて任せないでください。

  3. UX の細部が重要。Loading 状態、エラー表示、楽観的更新——これらの小さな点が、アプリが「まあまあ」か「本当に使いやすい」かを決めます。useActionStateuseFormStatus を組み合わせて、すべてをきちんと処理しましょう。

最もシンプルなフォームから試してみてください。Server Action を作り、Zod バリデーションを追加し、Loading を表示——これで 80% の使い方をマスターできます。残り 20%(キャッシュ更新、楽観的更新など)は必要になったときに公式ドキュメントを参照。

Next.js と React は急速に進化しており、Server Actions の API も変わる可能性があります。公式ドキュメントの更新をフォローし、この記事のコードがすぐに古くならないよう注意してください。

さあ、プロジェクトで試してみましょう。次にフォーム送信を書くとき、こんなにシンプルでよかった、と気づくかもしれません。

Server Actions でフォームを処理する完全フロー

Server Action の作成からバリデーション追加、状態管理までの全ステップ

⏱️ 目安時間: 30 分

  1. 1

    ステップ1: Server Action を作成する

    app/actions.ts で Server Action を作成:

    1. ファイルレベルマーク:ファイル先頭に 'use server' を追加
    2. 関数定義:export async function actionName(formData: FormData)
    3. データ取得:formData.get('fieldName') でフィールド値を取得
    4. 結果を返す:{ success: boolean, errors?: {}, message?: string } 形式で返す

    例:
    ```typescript
    'use server'
    export async function signup(formData: FormData) {
    const name = formData.get('name') as string
    // 処理ロジック...
    return { success: true, message: '登録成功' }
    }
    ```
  2. 2

    ステップ2: Zod バリデーションを追加する

    Zod でサーバー側データ検証:

    1. Zod をインストール:npm install zod
    2. Schema 定義:const SignupSchema = z.object({ name: z.string().min(2), email: z.string().email() })
    3. データ検証:const result = SignupSchema.safeParse(rawData)
    4. エラー処理:if (!result.success) return { success: false, errors: result.error.flatten().fieldErrors }

    ポイント:
    • safeParse は例外を投げず、{ success, data/error } を返す
    • flatten().fieldErrors で { field: ['error1'] } 形式に変換
    • 検証失敗時は構造化エラーを返し、クライアントで表示
  3. 3

    ステップ3: useActionState で状態を管理する

    クライアントコンポーネントで useActionState を使用:

    1. Hook をインポート:import { useActionState } from 'react'
    2. 初期状態を定義:const initialState = { success: false, errors: {} }
    3. Hook を使用:const [state, formAction, isPending] = useActionState(action, initialState)
    4. フォームにバインド:<form action={formAction}>
    5. エラー表示:{state.errors?.field && <p>{state.errors.field[0]}</p>}
    6. Loading 表示:<button disabled={isPending}>{isPending ? '送信中...' : '送信'}</button>

    フロー:
    • ユーザー送信 → action 呼び出し → 結果返却 → state 更新 → コンポーネント再レンダリング
  4. 4

    ステップ4: 認証と認可チェックを追加する

    Server Action にセキュリティチェックを追加:

    1. 入力検証:Zod で全入力を検証
    2. 認証:session token を確認
    ```typescript
    const cookieStore = await cookies()
    const sessionToken = cookieStore.get('session')?.value
    if (!sessionToken) return { success: false, error: '先にログインしてください' }
    ```
    3. 認可:操作権限を確認
    ```typescript
    const post = await getPost(postId)
    if (post.authorId !== currentUser.id) {
    return { success: false, error: '権限がありません' }
    }
    ```
    4. 操作実行:検証通過後にビジネスロジックを実行

    覚えておくこと:Server Actions は魔法ではなく、手動でセキュリティチェックが必要
  5. 5

    ステップ5: ユーザー体験を最適化する

    Loading 状態とエラー処理を追加:

    1. useFormStatus(ボタンコンポーネント内):
    ```typescript
    'use client'
    import { useFormStatus } from 'react-dom'
    export function SubmitButton() {
    const { pending } = useFormStatus()
    return <button disabled={pending}>...</button>
    }
    ```
    2. revalidatePath でキャッシュを更新:
    ```typescript
    import { revalidatePath } from 'next/cache'
    revalidatePath('/posts')
    ```
    3. 楽観的更新(任意、成功率の高い操作向け):
    ```typescript
    const [optimisticState, setOptimisticState] = useOptimistic(initialState)
    ```

    ベストプラクティス:
    • フォームロジックが複雑 → useActionState
    • 独立したボタンコンポーネント → useFormStatus
    • 操作成功後 → 関連ページのキャッシュを更新

FAQ

Server Actions と API Routes の違いは?いつどちらを使う?
Server Actions は内部フォーム送信とデータ変更向け。POST のみ、型安全、コード量が少ない。API Routes は外部 RESTful API、GET リクエスト、サードパーティ連携向け。シンプルに言えば、内部は Server Actions、外部は API Routes。
Server Actions は安全?どんな対策が必要?
Server Actions はサーバーで実行されますが、手動でセキュリティチェックが必要です:1) 入力検証(Zod)、2) 認証(session 確認)、3) 認可(操作権限の確認)。フレームワークは基本的な CSRF 対策のみ。自動セキュリティに頼ってはいけません。
useActionState と useFormStatus の違いは?
useActionState の isPending はフォームコンポーネント内で使い、state と pending を同時に取得できます。useFormStatus の pending はフォームの子コンポーネント(ボタンなど)内でのみ使え、pending 状態のみ取得可能。フォームロジックが複雑なら useActionState、独立ボタンなら useFormStatus。
フォームフィールド以外のパラメータはどう渡す?
2 つの方法:1) 非表示フィールド <input type="hidden" name="postId" value={postId} />、2) bind メソッド:const actionWithId = action.bind(null, postId)、<form action={actionWithId}>。bind メソッドを推奨。よりエレガントです。
フォーム送信後にページデータを更新するには?
revalidatePath でパス単位:revalidatePath('/posts')、または revalidateTag でタグ単位(fetch 時にタグ付けが必要)。パスが固定で数が少ない → revalidatePath、データが複数ページに分散 → revalidateTag。
いつ楽観的更新を使う?
成功率が非常に高い操作(いいね、お気に入り)向け。useOptimistic Hook で UI を即座に更新し、バックグラウンドで送信。失敗の可能性がある操作には非推奨。失敗時の UI ロールバックが面倒になります。

5分で読めます · 公開日: 2025年12月19日 · 更新日: 2026年6月8日

関連記事

コメント

GitHubアカウントでログインしてコメントできます