9.

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

編集

この記事の要点

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

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

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 にまとめられて関数の引数に渡ります。onSubmitpreventDefaultfetch も書きません。

サーバーコンポーネント上のフォームであれば、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 が見つからない」:インスタンスごとに暗号鍵が変わるためです。共通の鍵を環境変数で指定します(本番ビルドとセルフホスト)。

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

編集
Post Share
子ページ

子ページはありません

同階層のページ
  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 への移行と注意点