コンテンツにスキップ

Server Actions を使用したフォームの作成方法

React Server Actions は、サーバーで実行されるServer Functionsです。Server Components および Client Components から呼び出してフォーム送信を処理できます。このガイドでは、Next.js で Server Actions を使用してフォームを作成する方法を説明します。

仕組み

React では、HTML の <form> 要素を拡張して、Server Actions を action 属性で呼び出せるようにしています。

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

app/invoices/page.tsx
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'),
    }
 
    // mutate data
    // revalidate the cache
  }
 
  return <form action={createInvoice}>...</form>
}

知っておくと便利: 複数のフィールドを持つフォームを扱う場合は、JavaScript の Object.fromEntries() を使用します。例: const rawFormData = Object.fromEntries(formData)。このオブジェクトには $ACTION_ で始まる追加のプロパティが含まれることに注意してください。

追加引数の渡し方

フォームフィールドの外では、JavaScript の bind メソッドを使用して、Server Function に追加の引数を渡すことができます。たとえば、updateUser Server Function に userId 引数を渡す場合です。

app/client-component.tsx
'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">Update User Name</button>
    </form>
  )
}

Server Function は userId を追加の引数として受け取ります。

app/actions.ts
'use server'
 
export async function updateUser(userId: string, formData: FormData) {}

知っておくと良いこと:

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

フォームのバリデーション

フォームはクライアントまたはサーバーでバリデーションできます。

  • クライアントサイドバリデーションには、基本的なバリデーションのために requiredtype="email" のような HTML 属性を使用できます。
  • サーバーサイドバリデーションには、zod のようなライブラリを使用してフォームフィールドをバリデーションできます。例:
app/actions.ts
'use server'
 
import { z } from 'zod'
 
const schema = z.object({
  email: z.string({
    invalid_type_error: 'Invalid Email',
  }),
})
 
export default async function createUser(formData: FormData) {
  const validatedFields = schema.safeParse({
    email: formData.get('email'),
  })
 
  // Return early if the form data is invalid
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
    }
  }
 
  // Mutate data
}

バリデーションエラー

バリデーションエラーやメッセージを表示するには、<form> を定義するコンポーネントを Client Component に変換し、React の useActionState を使用します。

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

app/actions.ts
'use server'
 
import { z } from 'zod'
 
export async function createUser(initialState: any, formData: FormData) {
  const validatedFields = schema.safeParse({
    email: formData.get('email'),
  })
  // ...
}

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

app/ui/signup.tsx
'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">Email</label>
      <input type="text" id="email" name="email" required />
      {/* ... */}
      <p aria-live="polite">{state?.message}</p>
      <button disabled={pending}>Sign up</button>
    </form>
  )
}

保留状態

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

app/ui/signup.tsx
'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}>
      {/* Other form elements */}
      <button disabled={pending}>Sign up</button>
    </form>
  )
}

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

app/ui/button.tsx
'use client'
 
import { useFormStatus } from 'react-dom'
 
export function SubmitButton() {
  const { pending } = useFormStatus()
 
  return (
    <button disabled={pending} type="submit">
      Sign Up
    </button>
  )
}

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

app/ui/signup.tsx
import { SubmitButton } from './button'
import { createUser } from '@/app/actions'
 
export function Signup() {
  return (
    <form action={createUser}>
      {/* Other form elements */}
      <SubmitButton />
    </form>
  )
}

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

楽観的更新

React の useOptimistic フックを使用して、Server Function の実行が完了するのを待つのではなく、UI を楽観的に更新できます。

app/page.tsx
'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">Send</button>
      </form>
    </div>
  )
}

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

<form> の中にネストされた要素 (例: <button><input type="submit"><input type="image">) で Server Actions を呼び出すことができます。これらの要素は formAction プロップまたはイベントハンドラーを受け入れます。

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

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

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

app/entry.tsx
'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> の祖先が送信され、Server Function が呼び出されます。