◀ 9.

Next.js の Server Functions|フォームと更新処理

▶

この記事の要点

  • 'use server' を付けた非同期関数はサーバーで実行される関数になり、クライアントから直接呼べる。API を自分で作らなくてよい。
  • form の action に渡すと、FormData が自動的に引数として届く。
  • UI を経由せず直接 POST できる。認証と認可の確認は必ず関数の中で行う。
  • 送信中の状態は useActionState が返す pending で表現できる。
  • 更新後は updateTag や revalidatePath でキャッシュを整え、必要なら redirect で遷移する。

従来、ブラウザからデータを更新するには API エンドポイントを作り、fetch で呼び、レスポンスを見て画面を更新する、という手順が必要でした。Next.js の Server Functions はこの往復をフレームワーク側が引き受けます。関数を書いてフォームに渡すだけで、通信も画面更新もつながります。フォーム送信の文脈で使うものを特に Server Actions と呼びます。

1定義する

非同期関数の本体先頭、またはファイルの先頭に 'use server' と書きます。ファイル先頭に書いた場合は、そのファイルのすべてのエクスポートが対象になります。

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

import { auth } from '@/lib/auth'

export async function createPost(formData: FormData) {
  const session = await auth()
  if (!session?.user) {
    throw new Error('Unauthorized')
  }

  const title = formData.get('title')
  const content = formData.get('content')

  // データを保存する
  // キャッシュを整える
}

サーバーコンポーネントの中に直接書くこともできます。小さな処理ならこちらが手軽です。

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

  return <form action={createPost}>{/* ... */}</form>
}

クライアントコンポーネントの中で定義することはできません。別ファイルに 'use server' を付けて置き、それを import して使います。

2フォームから呼ぶ

もっとも基本的な呼び出し方が、form の action に関数そのものを渡す形です。

import { createPost } from '@/app/lib/actions'

export function Form() {
  return (
    <form action={createPost}>
      <input type="text" name="title" />
      <textarea name="content" />
      <button type="submit">作成</button>
    </form>
  )
}

送信すると、name 属性を持つ入力値が FormData にまとめられて関数の引数に渡ります。onSubmit も preventDefault も fetch も書きません。

サーバーコンポーネント上のフォームであれば、JavaScript がまだ読み込まれていない状態でも送信が成立します(プログレッシブエンハンスメント)。クライアントコンポーネント上のフォームの場合は、送信が一時的に待ち行列に入り、ハイドレーション完了後に処理されます。

ブラウザ form の submit (内部的には POST) Server Function ① 認証・認可の確認 ② データ更新 ③ キャッシュ無効化 更新後の UI と新しいデータを 1 往復で返す 重要な性質 この POST は画面を経由しなくても直接投げられる = 認可チェックを UI 側だけに置くのは無防備。関数の中で必ず確認する
Server Function は公開されたエンドポイントと同じ性質を持つ。ボタンを隠しただけでは守れない。

3イベントハンドラから呼ぶ

フォーム以外からも呼べます。クライアントコンポーネントで import して、通常の非同期関数のように扱います。

'use client'

import { incrementLike } from './actions'
import { useState } from 'react'

export default function LikeButton({ initialLikes }: { initialLikes: number }) {
  const [likes, setLikes] = useState(initialLikes)

  return (
    <>
      <p>いいね: {likes}</p>
      <button
        onClick={async () => {
          const updated = await incrementLike()
          setLikes(updated)
        }}
      >
        いいね
      </button>
    </>
  )
}

ただし、クライアントからの呼び出しは 1 つずつ順番に処理されます。並列にデータを取りたい場合は、サーバーコンポーネント側で取得するか、1 つの関数の中でまとめて処理してください。

4送信中の状態を出す

useActionState は、状態・アクション・pending フラグの 3 つを返します。二重送信の防止にも使えます。

'use client'

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

const initialState = { message: '' }

export function Form() {
  const [state, formAction, pending] = useActionState(createPost, initialState)

  return (
    <form action={formAction}>
      <input type="text" name="title" required />
      <textarea name="content" required />
      {state?.message && <p aria-live="polite">{state.message}</p>}
      <button disabled={pending}>作成</button>
    </form>
  )
}

この形にする場合、サーバー側の関数は第 1 引数に前回の状態を受け取る形になります。

'use server'

export async function createPost(prevState: any, formData: FormData) {
  const title = formData.get('title')
  // 検証に失敗したら「投げる」のではなく「返す」
  if (!title) {
    return { message: 'タイトルを入力してください' }
  }
  // ...
}

入力ミスのような想定内の失敗は例外にしないのが原則です。戻り値として表現し、画面にメッセージを出します。例外を投げるとエラー境界に落ち、フォームの入力内容ごと画面が差し替わってしまいます。この考え方は エラーハンドリングとエラー境界 で詳しく扱います。

5更新後の後始末

データを書き換えたら、表示を新しい状態に合わせる必要があります。用途に応じて使い分けます。

やりたいこと使う API
自分の変更を即座に画面へ反映updateTag('user-1')
他の閲覧者にも少し遅れて反映revalidateTag('posts', 'max')
特定のパスをまとめて作り直すrevalidatePath('/posts')
表示だけを取り直すrefresh()
完了後に別ページへ移動redirect('/posts')
'use server'

import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'

export async function createPost(formData: FormData) {
  // 保存処理
  revalidatePath('/posts')
  redirect('/posts')
}

redirect は制御用の例外を投げる実装になっているため、その後ろのコードは実行されません。キャッシュの無効化は必ず redirect より前に書いてください。

6Cookie を操作する

Server Function の中では cookies() で取得・設定・削除ができます。設定や削除を行うと、現在のページとレイアウトがサーバー側で再描画され、新しい値が反映された状態になります。

'use server'

import { cookies } from 'next/headers'

export async function setTheme(value: string) {
  const cookieStore = await cookies()
  cookieStore.set('theme', value)
}

7つまずきやすいところ

セキュリティ
Server Function は POST で直接叩けます。「管理者にしかボタンを見せていない」は防御になりません。すべての関数の冒頭でログイン状態と、その資源に対する権限を確認してください。
設計の型
①認可 → ②入力検証 → ③更新 → ④キャッシュ無効化 → ⑤(必要なら)遷移、の順で書くと抜けが出にくくなります。
  • クライアントコンポーネントの中で 'use server' を書いてしまう:定義はできません。別ファイルに切り出します。
  • props で渡すときの命名:クライアントコンポーネントへ関数を渡す場合、updateItemAction のように Action で終わる名前にしておくと、リンターの支援を受けやすくなります。
  • 戻り値が渡せない:クライアントへ返る値は React がシリアライズできるものに限られます。クラスインスタンスなどはそのまま返せません。
  • 複数台構成で「Server Action が見つからない」:インスタンスごとに暗号鍵が変わるためです。共通の鍵を環境変数で指定します(本番ビルドとセルフホスト)。

更新処理が書けるようになったら、失敗したときの振る舞いを設計します。

子ページ

子ページはありません

同階層のページ
  1. 開発環境の構築とプロジェクトの初期構成
  2. App Router のルーティングとファイル規約
  3. レイアウトと特殊ファイルの使い分け
  4. サーバーコンポーネントとクライアントコンポーネント
  5. サーバー側でのデータ取得と並列化
  6. ストリーミングで体感速度を上げる
  7. キャッシュの仕組みと use cache
  8. 再検証の使い分け
  9. フォーム送信とデータ更新の実装
  10. エラーハンドリングとエラー境界
  11. Route Handlers で API を作る
  12. メタデータ API で SEO を整える
  13. 画像とフォントの最適化
  14. 本番ビルドとセルフホスト
  15. バージョン16 への移行と注意点

最近更新/作成されたページ