フォームとデータ変更 (Forms and Mutations)

フォームを使用すると、Webアプリケーションでデータの作成や更新が可能になります。Next.js では Server Actions を使用してフォーム送信とデータ変更を強力に処理できます。

Server Actions の仕組み

Server Actions を使用すると、APIエンドポイントを手動で作成する必要がありません。代わりに、コンポーネントから直接呼び出せる非同期サーバー関数を定義できます。

🎥 動画で学ぶ: App Router でのフォームとデータ変更について → YouTube (10分)

Server Actions は Server Components で定義したり、Client Components から呼び出したりできます。Server Component でアクションを定義すると、JavaScriptがなくてもフォームが機能するようになり、プログレッシブエンハンスメントを実現します。

next.config.js ファイルで Server Actions を有効にします:

next.config.js
module.exports = {
  experimental: {
    serverActions: true,
  },
}

知っておくと良いこと:

  • Server Components から Server Actions を呼び出すフォームは、JavaScriptがなくても機能します。
  • Client Components から Server Actions を呼び出すフォームは、JavaScriptがまだ読み込まれていない場合、送信をキューに入れ、クライアントのハイドレーションを優先します。
  • Server Actions は使用されるページやレイアウトの ランタイム を継承します。
  • Server Actions は完全に静的なルート(ISRによるデータの再検証を含む)でも動作します。

キャッシュデータの再検証

Server Actions は Next.js の キャッシュと再検証 アーキテクチャと深く統合されています。フォームが送信されると、Server Action はキャッシュデータを更新し、変更が必要なキャッシュキーを再検証できます。

従来のアプリケーションのようにルートごとに1つのフォームに制限されるのではなく、Server Actions ではルートごとに複数のアクションを持つことができます。さらに、フォーム送信時にブラウザをリフレッシュする必要がありません。Next.js は1回のネットワーク往復で、更新されたUIとリフレッシュされたデータの両方を返すことができます。

Server Actions からのデータ再検証 の例をご覧ください。

サーバー専用フォーム

サーバー専用フォームを作成するには、Server Component で Server Action を定義します。アクションは、関数の先頭に "use server" ディレクティブを付けてインラインで定義するか、ファイルの先頭にディレクティブを付けて別ファイルに定義できます。

export default function Page() {
  async function create(formData: FormData) {
    'use server'

    // データを変更
    // キャッシュを再検証
  }

  return <form action={create}>...</form>
}
export default function Page() {
  async function create(formData) {
    'use server'

    // データを変更
    // キャッシュを再検証
  }

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

知っておくと良いこと: <form action={create}>FormData データ型を受け取ります。上記の例では、HTML form 経由で送信された FormData がサーバーアクション create でアクセス可能です。

データの再検証

Server Actions を使用すると、Next.js キャッシュ をオンデマンドで無効化できます。revalidatePath でルートセグメント全体を無効化できます:

'use server'

import { revalidatePath } from 'next/cache'

export default async function submit() {
  await submitForm()
  revalidatePath('/')
}
'use server'

import { revalidatePath } from 'next/cache'

export default async function submit() {
  await submitForm()
  revalidatePath('/')
}

または、revalidateTag を使用してキャッシュタグで特定のデータフェッチを無効化できます:

'use server'

import { revalidateTag } from 'next/cache'

export default async function submit() {
  await addPost()
  revalidateTag('posts')
}
'use server'

import { revalidateTag } from 'next/cache'

export default async function submit() {
  await addPost()
  revalidateTag('posts')
}

リダイレクト

Server Action 完了後にユーザーを別のルートにリダイレクトしたい場合は、redirect と任意の絶対URLまたは相対URLを使用できます:

'use server'

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

export default async function submit() {
  const id = await addPost()
  revalidateTag('posts') // キャッシュされた投稿を更新
  redirect(`/post/${id}`) // 新しいルートに移動
}
'use server'

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

export default async function submit() {
  const id = await addPost()
  revalidateTag('posts') // キャッシュされた投稿を更新
  redirect(`/post/${id}`) // 新しいルートに移動
}

フォーム検証

基本的なフォーム検証には、requiredtype="email" などのHTML検証を使用することを推奨します。

より高度なサーバーサイド検証には、zod などのスキーマ検証ライブラリを使用して、解析されたフォームデータの構造を検証します:

import { z } from 'zod'

const schema = z.object({
  // ...
})

export default async function submit(formData: FormData) {
  const parsed = schema.parse({
    id: formData.get('id'),
  })
  // ...
}
import { z } from 'zod'

const schema = z.object({
  // ...
})

export default async function submit(formData) {
  const parsed = schema.parse({
    id: formData.get('id'),
  })
  // ...
}

ローディング状態の表示

useFormStatus フックを使用して、フォームがサーバーで送信されている間のローディング状態を表示します。useFormStatus フックは、Server Action を使用する form 要素の子としてのみ使用できます。

例えば、次の送信ボタン:

'use client'

import { experimental_useFormStatus as useFormStatus } from 'react-dom'

export function SubmitButton() {
  const { pending } = useFormStatus()

  return (
    <button type="submit" aria-disabled={pending}>
      Add
    </button>
  )
}
'use client'

import { experimental_useFormStatus as useFormStatus } from 'react-dom'

export function SubmitButton() {
  const { pending } = useFormStatus()

  return (
    <button type="submit" aria-disabled={pending}>
      Add
    </button>
  )
}

<SubmitButton /> は Server Action を持つフォームで使用できます:

import { SubmitButton } from '@/app/submit-button'

export default async function Home() {
  return (
    <form action={...}>
      <input type="text" name="field-name" />
      <SubmitButton />
    </form>
  )
}
import { SubmitButton } from '@/app/submit-button'

export default async function Home() {
  return (
    <form action={...}>
      <input type="text" name="field-name" />
      <SubmitButton />
    </form>
  )
}

エラーハンドリング

サーバーアクション (Server Actions) はシリアライズ可能なオブジェクトを返すこともできます。例えば、新しいアイテム作成時のエラーを処理するサーバーアクションは次のようになります:

'use server'

export async function createTodo(prevState: any, formData: FormData) {
  try {
    await createItem(formData.get('todo'))
    return revalidatePath('/')
  } catch (e) {
    return { message: 'Failed to create' }
  }
}
'use server'

export async function createTodo(prevState, formData) {
  try {
    await createItem(formData.get('todo'))
    return revalidatePath('/')
  } catch (e) {
    return { message: 'Failed to create' }
  }
}

そして、クライアントコンポーネント (Client Component) からこの値を読み取り、エラーメッセージを表示できます。

'use client'

import { experimental_useFormState as useFormState } from 'react-dom'
import { experimental_useFormStatus as useFormStatus } from 'react-dom'
import { createTodo } from '@/app/actions'

const initialState = {
  message: null,
}

function SubmitButton() {
  const { pending } = useFormStatus()

  return (
    <button type="submit" aria-disabled={pending}>
      追加
    </button>
  )
}

export function AddForm() {
  const [state, formAction] = useFormState(createTodo, initialState)

  return (
    <form action={formAction}>
      <label htmlFor="todo">タスクを入力</label>
      <input type="text" id="todo" name="todo" required />
      <SubmitButton />
      <p aria-live="polite" className="sr-only">
        {state?.message}
      </p>
    </form>
  )
}
'use client'

import { experimental_useFormState as useFormState } from 'react-dom'
import { experimental_useFormStatus as useFormStatus } from 'react-dom'
import { createTodo } from '@/app/actions'

const initialState = {
  message: null,
}

function SubmitButton() {
  const { pending } = useFormStatus()

  return (
    <button type="submit" aria-disabled={pending}>
      追加
    </button>
  )
}

export function AddForm() {
  const [state, formAction] = useFormState(createTodo, initialState)

  return (
    <form action={formAction}>
      <label htmlFor="todo">タスクを入力</label>
      <input type="text" id="todo" name="todo" required />
      <SubmitButton />
      <p aria-live="polite" className="sr-only">
        {state?.message}
      </p>
    </form>
  )
}

楽観的更新 (Optimistic Updates)

サーバーアクション (Server Action) の完了を待たずにUIを更新するには、useOptimistic を使用します:

'use client'

import { experimental_useOptimistic as useOptimistic } from 'react'
import { send } from './actions'

type Message = {
  message: string
}

export function Thread({ messages }: { messages: Message[] }) {
  const [optimisticMessages, addOptimisticMessage] = useOptimistic<Message[]>(
    messages,
    (state: Message[], newMessage: string) => [
      ...state,
      { message: newMessage },
    ]
  )

  return (
    <div>
      {optimisticMessages.map((m, k) => (
        <div key={k}>{m.message}</div>
      ))}
      <form
        action={async (formData: FormData) => {
          const message = formData.get('message')
          addOptimisticMessage(message)
          await send(message)
        }}
      >
        <input type="text" name="message" />
        <button type="submit">送信</button>
      </form>
    </div>
  )
}
'use client'

import { experimental_useOptimistic as useOptimistic } from 'react'
import { send } from './actions'

export function Thread({ messages }) {
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state, newMessage) => [...state, { message: newMessage }]
  )

  return (
    <div>
      {optimisticMessages.map((m) => (
        <div>{m.message}</div>
      ))}
      <form
        action={async (formData) => {
          const message = formData.get('message')
          addOptimisticMessage(message)
          await send(message)
        }}
      >
        <input type="text" name="message" />
        <button type="submit">送信</button>
      </form>
    </div>
  )
}

クッキーの設定

サーバーアクション (Server Action) 内で cookies 関数を使用してクッキーを設定できます:

'use server'

import { cookies } from 'next/headers'

export async function create() {
  const cart = await createCart()
  cookies().set('cartId', cart.id)
}
'use server'

import { cookies } from 'next/headers'

export async function create() {
  const cart = await createCart()
  cookies().set('cartId', cart.id)
}

クッキーの読み取り

サーバーアクション内で cookies 関数を使用してクッキーを読み取れます:

'use server'

import { cookies } from 'next/headers'

export async function read() {
  const auth = cookies().get('authorization')?.value
  // ...
}
'use server'

import { cookies } from 'next/headers'

export async function read() {
  const auth = cookies().get('authorization')?.value
  // ...
}

クッキーの削除

サーバーアクション内で cookies 関数を使用してクッキーを削除できます:

'use server'

import { cookies } from 'next/headers'

export async function delete() {
  cookies().delete('name')
  // ...
}
'use server'

import { cookies } from 'next/headers'

export async function delete() {
  cookies().delete('name')
  // ...
}

サーバーアクションからのクッキー削除の追加例を参照してください。