Menu

Server Actionsでフォームを作成する方法

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

動作の仕組み

Reactは、HTML <form>要素を拡張し、action属性を使用してServer Actionsを呼び出すことができるようにしています。

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

app/invoices/page.tsx
TypeScript
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()を使用します。例えば、const rawFormData = Object.fromEntries(formData)のようにします。このオブジェクトには$ACTION_というプレフィックスが付いた追加のプロパティが含まれることに注意してください。

追加の引数を渡す

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

app/client-component.tsx
TypeScript
'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>
  )
}

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

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

補足:

  • 別の方法として、引数をフォーム内の非表示入力フィールドとして渡すことができます(例えば、<input type="hidden" name="userId" value={userId} />)。ただし、この値はレンダリングされたHTMLの一部となり、エンコードされません。
  • bindはServer ComponentとClient Componentの両方で機能し、段階的な強化をサポートしています。

フォーム検証

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

  • クライアント側検証の場合、基本的な検証のためにrequiredtype="email"などのHTML属性を使用できます。
  • サーバー側検証の場合、zodなどのライブラリを使用してフォームフィールドを検証できます。例えば、以下のようにします。
app/actions.ts
TypeScript
'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>を定義するコンポーネントをClient Componentに変更し、React useActionStateを使用します。

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

app/actions.ts
TypeScript
'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
TypeScript
'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ブール値を公開しています。

app/ui/signup.tsx
TypeScript
'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フックを使用して、アクションの実行中にローディングインジケーターを表示することができます。このフックを使用する場合は、ローディングインジケーターをレンダリングするための別のコンポーネントを作成する必要があります。例えば、アクションが保留中の場合にボタンを無効化するには、以下のようにします。

app/ui/button.tsx
TypeScript
'use client'
 
import { useFormStatus } from 'react-dom'
 
export function SubmitButton() {
  const { pending } = useFormStatus()
 
  return (
    <button disabled={pending} type="submit">
      サインアップ
    </button>
  )
}

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

app/ui/signup.tsx
TypeScript
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フックを使用して、Server Functionの実行が終了するのを待つのではなく、その前にUIを楽観的に更新できます。

app/page.tsx
TypeScript
'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>
  )
}

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

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

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

プログラマティックなフォーム送信

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

app/entry.tsx
TypeScript
'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が呼び出されます。