125k

Next.js

useActionState とサーバー アクションを使用して React でフォームを構築します。

このガイドでは、useActionState とサーバー アクションを使用して Next.js でフォームを構築する方法を見ていきます。フォームの構築、検証、保留状態、アクセシビリティなどについて説明します。

デモ

単純なテキスト入力とテキストエリアを備えた次のフォームを構築します。送信時に、サーバー アクションを使用してフォーム データを検証し、フォームの状態を更新します。

コンポーネント form-next-demo が見つかりません。

アプローチ

このフォームは、Next.js と React の組み込み機能を利用してフォームを処理します。 <Field /> コンポーネントを使用してフォームを構築します。これにより、マークアップとスタイルを完全に柔軟に行うことができます。

  • ナビゲーションと段階的な機能強化に Next.js の <Form /> コンポーネントを使います。
  • アクセシブルなフォームの構築に <Field /> コンポーネントを使います。
  • useActionState でフォームの状態とエラーを管理します。
  • 保留中のプロパティを使用してロード状態を処理します。
  • フォーム送信の処理にサーバー アクションを使います。
  • Zod を使用したサーバー側の検証。

構成

<Field /> コンポーネントを使用したフォームの基本的な例を次に示します。

<Form action={formAction}>
  <FieldGroup>
    <Field data-invalid={!!formState.errors?.title?.length}>
      <FieldLabel htmlFor="title">Bug Title</FieldLabel>
      <Input
        id="title"
        name="title"
        defaultValue={formState.values.title}
        disabled={pending}
        aria-invalid={!!formState.errors?.title?.length}
        placeholder="Login button not working on mobile"
        autoComplete="off"
      />
      <FieldDescription>
        Provide a concise title for your bug report.
      </FieldDescription>
      {formState.errors?.title && (
        <FieldError>{formState.errors.title[0]}</FieldError>
      )}
    </Field>
  </FieldGroup>
  <Button type="submit">Submit</Button>
</Form>

使い方

フォーム スキーマを作成する

まず、schema.ts ファイル内の Zod スキーマを使用してフォームの形状を定義します。Zod は、標準スキーマでサポートされている他のスキーマ検証ライブラリに置き換えられます。

schema.ts
import { z } from "zod"
 
export const formSchema = z.object({
  title: z
    .string()
    .min(5, "Bug title must be at least 5 characters.")
    .max(32, "Bug title must be at most 32 characters."),
  description: z
    .string()
    .min(20, "Description must be at least 20 characters.")
    .max(100, "Description must be at most 100 characters."),
})

フォーム状態の型を定義する

次に、値、エラー、成功状態を含むフォーム状態の型を作成します。この型はクライアントとサーバーのフォーム状態に使用します。

schema.ts
import { z } from "zod"
 
export type FormState = {
  values?: z.infer<typeof formSchema>
  errors: null | Partial<Record<keyof z.infer<typeof formSchema>, string[]>>
  success: boolean
}

重要: スキーマと FormState タイプを別のファイルで定義して、それらをクライアント コンポーネントとサーバー コンポーネントの両方にインポートできるようにします。

サーバー アクションを作成する

サーバー アクションはサーバー上で実行され、クライアントから呼び出せる関数です。フォーム データの検証とフォーム状態の更新に使います。

注: エラーの場合は values を返します。これは、ユーザーが送信した値をフォームの状態に維持したいためです。成功した場合は、空の値を返してフォームをリセットします。

フォームの構築

<Field /> コンポーネントを使用してフォームを構築できるようになりました。 useActionState フックを使用して、フォームの状態、サーバー アクション、および保留状態を管理します。

完了

これで、クライアント側とサーバー側の検証を備えた、完全にアクセシブルなフォームが完成しました。

フォームを送信すると、formAction 関数がサーバー上で呼び出されます。サーバー アクションはフォーム データを検証し、フォームの状態を更新します。

フォーム データが無効な場合、サーバー アクションはエラーをクライアントに返します。有効な場合は成功状態を返し、フォーム状態を更新します。

保留中の状態

useActionState の pending プロパティを使用して、読み込みインジケーターを表示し、フォーム入力を無効にします。

"use client"
 
import * as React from "react"
import Form from "next/form"
 
import { Spinner } from "@/components/ui/spinner"
 
import { bugReportFormAction } from "./actions"
 
export function BugReportForm() {
  const [formState, formAction, pending] = React.useActionState(
    bugReportFormAction,
    {
      errors: null,
      success: false,
    }
  )
 
  return (
    <Form action={formAction}>
      <FieldGroup>
        <Field data-disabled={pending}>
          <FieldLabel htmlFor="name">Name</FieldLabel>
          <Input id="name" name="name" disabled={pending} />
        </Field>
        <Field>
          <Button type="submit" disabled={pending}>
            {pending && <Spinner />} Submit
          </Button>
        </Field>
      </FieldGroup>
    </Form>
  )
}

無効な状態

送信ボタン

送信ボタンを無効にするには、ボタンの disabled プロップで pending プロップを使用します。

<Button type="submit" disabled={pending}>
  {pending && <Spinner />} Submit
</Button>

フィールド

<Field /> コンポーネントに無効な状態とスタイルを適用するには、<Field /> コンポーネントで data-disabled プロパティを使用します。

<Field data-disabled={pending}>
  <FieldLabel htmlFor="name">Name</FieldLabel>
  <Input id="name" name="name" disabled={pending} />
</Field>

バリデーション

サーバー側検証

サーバー アクションのスキーマで safeParse() を使用して、フォーム データを検証します。

actions.ts
"use server"
 
export async function bugReportFormAction(
  _prevState: FormState,
  formData: FormData
) {
  const values = {
    title: formData.get("title") as string,
    description: formData.get("description") as string,
  }
 
  const result = formSchema.safeParse(values)
 
  if (!result.success) {
    return {
      values,
      success: false,
      errors: result.error.flatten().fieldErrors,
    }
  }
 
  return {
    errors: null,
    success: true,
  }
}

ビジネス ロジックの検証

サーバー アクションにカスタム検証ロジックを追加できます。

検証エラーが発生した場合は必ず値を返します。これは、フォームの状態がユーザーの入力を確実に維持するためです。

actions.ts
"use server"
 
export async function bugReportFormAction(
  _prevState: FormState,
  formData: FormData
) {
  const values = {
    title: formData.get("title") as string,
    description: formData.get("description") as string,
  }
 
  const result = formSchema.safeParse(values)
 
  if (!result.success) {
    return {
      values,
      success: false,
      errors: result.error.flatten().fieldErrors,
    }
  }
 
  // Check if email already exists in database.
  const existingUser = await db.user.findUnique({
    where: { email: result.data.email },
  })
 
  if (existingUser) {
    return {
      values,
      success: false,
      errors: {
        email: ["This email is already registered"],
      },
    }
  }
 
  return {
    errors: null,
    success: true,
  }
}

エラーの表示

<FieldError /> を使用してフィールドの横にエラーを表示します。 data-invalid プロパティを <Field /> コンポーネントに追加し、aria-invalid プロパティを入力に追加してください。

<Field data-invalid={!!formState.errors?.email?.length}>
  <FieldLabel htmlFor="email">Email</FieldLabel>
  <Input
    id="email"
    name="email"
    type="email"
    aria-invalid={!!formState.errors?.email?.length}
  />
  {formState.errors?.email && (
    <FieldError>{formState.errors.email[0]}</FieldError>
  )}
</Field>

フォームをリセット

サーバー アクションを使用してフォームを送信すると、React はフォームの状態を自動的に初期値にリセットします。

成功時にリセットする

成功時にフォームをリセットするには、サーバー アクションから values を省略すると、React がフォームの状態を自動的に初期値にリセットします。これは標準的な React の動作です。

actions.ts
export async function demoFormAction(
  _prevState: FormState,
  formData: FormData
) {
  const values = {
    title: formData.get("title") as string,
    description: formData.get("description") as string,
  }
 
  // Validation.
  if (!result.success) {
    return {
      values,
      success: false,
      errors: result.error.flatten().fieldErrors,
    }
  }
 
  // Business logic.
  callYourDatabaseOrAPI(values)
 
  // Omit the values on success to reset the form state.
  return {
    errors: null,
    success: true,
  }
}

検証エラー時に保持する

失敗時にフォームがリセットされないようにするには、サーバー アクションで値を返すことができます。これは、フォームの状態がユーザーの入力を確実に維持するためです。

actions.ts
export async function demoFormAction(
  _prevState: FormState,
  formData: FormData
) {
  const values = {
    title: formData.get("title") as string,
    description: formData.get("description") as string,
  }
 
  // Validation.
  if (!result.success) {
    return {
      // Return the values on validation errors.
      values,
      success: false,
      errors: result.error.flatten().fieldErrors,
    }
  }
}

複雑なフォーム

複数のフィールドと検証を含む、より複雑なフォームの例です。

コンポーネント form-next-complex が見つかりません。

スキーマ

フォーム

サーバー アクション