はじめに/ガイド/フォーム

サーバーアクションを使用したフォーム作成方法

Reactサーバーアクションは、サーバー上で実行されるサーバー関数です。サーバーコンポーネントとクライアントコンポーネントの両方で呼び出され、フォーム送信を処理できます。このガイドでは、Next.jsでサーバーアクションを使用してフォームを作成する方法を説明します。

動作原理

ReactはHTMLの<form>要素を拡張し、action属性でサーバーアクションを呼び出せるようにしています。

フォームで使用すると、関数は自動的にFormDataオブジェクトを受け取ります。その後、FormDataメソッドを使用してデータを抽出できます:

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

    const rawFormData = {
      customerId: formData.get('customerId'),
      amount: formData.get('amount'),
      status: formData.get('status'),
    }

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

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

補足: 複数のフィールドを持つフォームを扱う場合、JavaScriptのObject.fromEntries()entries()メソッドを使用できます。例:const rawFormData = Object.fromEntries(formData)

追加引数の渡し方

フォームフィールド以外で追加の引数をサーバー関数に渡すには、JavaScriptのbindメソッドを使用します。例えば、userId引数をupdateUserサーバー関数に渡す場合:

'use client'

import { updateUser } from './actions'

export function UserProfile({ userId }: { userId: string }) {
  const updateUserWithId = updateUser.bind(null, userId)

  return (
    <form action={updateUserWithId}>
      <input type="text" name="name" />
      <button type="submit">ユーザー名を更新</button>
    </form>
  )
}

サーバー関数はuserIdを追加引数として受け取ります:

'use server'

export async function updateUser(userId: string, formData: FormData) {}

補足:

  • 代替方法として、フォーム内に隠し入力フィールドとして引数を渡すこともできます(例:<input type="hidden" name="userId" value={userId} />)。ただし、値はレンダリングされたHTMLの一部となり、エンコードされません。
  • bindはサーバーコンポーネントとクライアントコンポーネントの両方で動作し、プログレッシブエンハンスメントをサポートします。

フォーム検証

フォームはクライアント側またはサーバー側で検証できます。

  • クライアント側検証には、基本的な検証のためにrequiredtype="email"などのHTML属性を使用できます。
  • サーバー側検証には、zodなどのライブラリを使用してフォームフィールドを検証できます。例:
'use server'

import { z } from 'zod'

const schema = z.object({
  email: z.string({
    invalid_type_error: '無効なメールアドレス',
  }),
})

export default async function createUser(formData: FormData) {
  const validatedFields = schema.safeParse({
    email: formData.get('email'),
  })

  // フォームデータが無効な場合は早期リターン
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
    }
  }

  // データを更新
}

検証エラー

検証エラーやメッセージを表示するには、<form>を定義するコンポーネントをクライアントコンポーネントに変更し、ReactのuseActionStateを使用します。

useActionStateを使用すると、サーバー関数のシグネチャが変更され、最初の引数として新しいprevStateまたはinitialStateパラメータを受け取るようになります。

'use server'

import { z } from 'zod'

export async function createUser(initialState: any, formData: FormData) {
  const validatedFields = schema.safeParse({
    email: formData.get('email'),
  })
  // ...
}
'use server'

import { z } from 'zod'

// ...

export async function createUser(initialState, formData) {
  const validatedFields = schema.safeParse({
    email: formData.get('email'),
  })
  // ...
}

その後、stateオブジェクトに基づいて条件付きでエラーメッセージをレンダリングできます。

'use client'

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

const initialState = {
  message: '',
}

export function Signup() {
  const [state, formAction, pending] = useActionState(createUser, initialState)

  return (
    <form action={formAction}>
      <label htmlFor="email">メールアドレス</label>
      <input type="text" id="email" name="email" required />
      {/* ... */}
      <p aria-live="polite">{state?.message}</p>
      <button disabled={pending}>登録</button>
    </form>
  )
}

保留状態

useActionStateフックは、アクション実行中にローディングインジケーターを表示したり送信ボタンを無効にしたりするために使用できるpendingブール値を公開します。

'use client'

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

export function Signup() {
  const [state, formAction, pending] = useActionState(createUser, initialState)

  return (
    <form action={formAction}>
      {/* 他のフォーム要素 */}
      <button disabled={pending}>登録</button>
    </form>
  )
}

あるいは、useFormStatusフックを使用して、アクション実行中にローディングインジケーターを表示することもできます。このフックを使用する場合、ローディングインジケーターをレンダリングするための別のコンポーネントを作成する必要があります。例えば、アクションが保留中にボタンを無効にするには:

'use client'

import { useFormStatus } from 'react-dom'

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

  return (
    <button disabled={pending} type="submit">
      登録
    </button>
  )
}

その後、SubmitButtonコンポーネントをフォーム内にネストできます:

import { SubmitButton } from './button'
import { createUser } from '@/app/actions'

export function Signup() {
  return (
    <form action={createUser}>
      {/* 他のフォーム要素 */}
      <SubmitButton />
    </form>
  )
}

補足: React 19では、useFormStatusは返されるオブジェクトにdata、method、actionなどの追加キーを含みます。React 19を使用していない場合、pendingキーのみが利用可能です。

楽観的更新

ReactのuseOptimisticフックを使用して、サーバー関数の実行が完了するのを待たずにUIを楽観的に更新できます:

'use client'

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

type Message = {
  message: string
}

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

  const formAction = async (formData: FormData) => {
    const message = formData.get('message') as string
    addOptimisticMessage(message)
    await send(message)
  }

  return (
    <div>
      {optimisticMessages.map((m, i) => (
        <div key={i}>{m.message}</div>
      ))}
      <form action={formAction}>
        <input type="text" name="message" />
        <button type="submit">送信</button>
      </form>
    </div>
  )
}

ネストされたフォーム要素

<button><input type="submit"><input type="image">など、<form>内にネストされた要素でサーバーアクションを呼び出せます。これらの要素はformActionプロップまたはイベントハンドラーを受け入れます。

これは、フォーム内で複数のサーバーアクションを呼び出したい場合に便利です。例えば、投稿を公開するだけでなく下書きとして保存するための特定の<button>要素を作成できます。詳細はReactの<form>ドキュメントを参照してください。

プログラムによるフォーム送信

requestSubmit()メソッドを使用して、プログラムでフォーム送信をトリガーできます。例えば、ユーザーが + Enterキーボードショートカットでフォームを送信する場合、onKeyDownイベントをリッスンできます:

'use client'

export function Entry() {
  const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
    if (
      (e.ctrlKey || e.metaKey) &&
      (e.key === 'Enter' || e.key === 'NumpadEnter')
    ) {
      e.preventDefault()
      e.currentTarget.form?.requestSubmit()
    }
  }

  return (
    <div>
      <textarea name="entry" rows={20} required onKeyDown={handleKeyDown} />
    </div>
  )
}

これにより、最も近い<form>祖先の送信がトリガーされ、サーバー関数が呼び出されます。

On this page