125k

React Hook Form

React Hook Form と Zod を使用して React でフォームを構築します。

このガイドでは、React Hook Form を使用してフォームを構築する方法を見ていきます。 <Field /> コンポーネントを使用したフォームの構築、Zod を使用したスキーマ検証の追加、エラー処理、アクセシビリティなどについて説明します。

デモ

次のフォームを構築します。単純なテキスト入力とテキストエリアがあります。送信時にフォーム データを検証し、エラーがあれば表示します。

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

アプローチ

このフォームは、React Hook Form を利用して、パフォーマンスが高く柔軟なフォーム処理を実現します。 <Field /> コンポーネントを使用してフォームを構築します。これにより、マークアップとスタイルを完全に柔軟に行うことができます。

  • フォームの状態管理に React Hook Form の useForm フックを使用します。
  • 制御された入力には <Controller /> コンポーネントを使います。
  • アクセシブルなフォームの構築には <Field /> コンポーネントを使います。
  • zodResolver で Zod を使用したクライアント側の検証。

構成

React Hook Form の <Controller /> コンポーネントと <Field /> コンポーネントを使った基本的なフォームの例です。

<Controller
  name="title"
  control={form.control}
  render={({ field, fieldState }) => (
    <Field data-invalid={fieldState.invalid}>
      <FieldLabel htmlFor={field.name}>Bug Title</FieldLabel>
      <Input
        {...field}
        id={field.name}
        aria-invalid={fieldState.invalid}
        placeholder="Login button not working on mobile"
        autoComplete="off"
      />
      <FieldDescription>
        Provide a concise title for your bug report.
      </FieldDescription>
      {fieldState.invalid && <FieldError errors={[fieldState.error]} />}
    </Field>
  )}
/>

使い方

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

まず、Zod スキーマを使用してフォームの形状を定義します。

form.tsx
import * as z from "zod"
 
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."),
})

フォームのセットアップ

次に、React Hook Form の useForm フックを使用してフォーム インスタンスを作成します。フォーム データを検証するために Zod リゾルバーも追加します。

form.tsx
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import * as z from "zod"
 
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."),
})
 
export function BugReportForm() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      title: "",
      description: "",
    },
  })
 
  function onSubmit(data: z.infer<typeof formSchema>) {
    // Do something with the form values.
    console.log(data)
  }
 
  return (
    <form onSubmit={form.handleSubmit(onSubmit)}>
      {/* ... */}
      {/* Build the form here */}
      {/* ... */}
    </form>
  )
}

フォームの構築

これで、React Hook Form の <Controller /> コンポーネントと <Field /> コンポーネントを使ってフォームを構築できます。

完了

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

フォームを送信すると、検証されたフォーム データを使用して onSubmit 関数が呼び出されます。フォームデータが無効な場合、React Hook Form は各フィールドの横にエラーを表示します。

バリデーション

クライアント側検証

React Hook Form は、Zod スキーマを使用してフォーム データを検証します。スキーマを定義し、それを useForm フックの resolver オプションに渡します。

example-form.tsx
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import * as z from "zod"
 
const formSchema = z.object({
  title: z.string(),
  description: z.string().optional(),
})
 
export function ExampleForm() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      title: "",
      description: "",
    },
  })
}

検証モード

React Hook Form はさまざまな検証モードをサポートしています。

form.tsx
const form = useForm<z.infer<typeof formSchema>>({
  resolver: zodResolver(formSchema),
  mode: "onChange",
})
モード説明
"onChange"変更が行われるたびに検証がトリガーされます。
"onBlur"ブラー時に検証がトリガーされます。
"onSubmit"検証は送信時にトリガーされます (デフォルト)。
"onTouched"検証は最初のブラー時にトリガーされ、その後はすべての変更時にトリガーされます。
"all"検証はブラーと変更時にトリガーされます。

エラーの表示

<FieldError /> を使用してフィールドの横にエラーを表示します。スタイルとアクセシビリティについて:

  • data-invalid プロパティを <Field /> コンポーネントに追加します。
  • <Input />、<SelectTrigger />、<Checkbox /> などのフォーム コントロールに aria-invalid プロパティを追加します。
form.tsx
<Controller
  name="email"
  control={form.control}
  render={({ field, fieldState }) => (
    <Field data-invalid={fieldState.invalid}>
      <FieldLabel htmlFor={field.name}>Email</FieldLabel>
      <Input
        {...field}
        id={field.name}
        type="email"
        aria-invalid={fieldState.invalid}
      />
      {fieldState.invalid && <FieldError errors={[fieldState.error]} />}
    </Field>
  )}
/>

さまざまなフィールド タイプの操作

入力

  • 入力フィールドの場合は、field オブジェクトを <Input /> コンポーネントに展開します。
  • エラーを表示するには、aria-invalid プロパティを <Input /> コンポーネントに追加し、data-invalid プロパティを <Field /> コンポーネントに追加します。

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

単純なテキスト入力では、field オブジェクトを入力に展開します。

form.tsx
<Controller
  name="name"
  control={form.control}
  render={({ field, fieldState }) => (
    <Field data-invalid={fieldState.invalid}>
      <FieldLabel htmlFor={field.name}>Name</FieldLabel>
      <Input {...field} id={field.name} aria-invalid={fieldState.invalid} />
      {fieldState.invalid && <FieldError errors={[fieldState.error]} />}
    </Field>
  )}
/>

テキストエリア

  • テキストエリア フィールドの場合は、field オブジェクトを <Textarea /> コンポーネントに展開します。
  • エラーを表示するには、aria-invalid プロパティを <Textarea /> コンポーネントに追加し、data-invalid プロパティを <Field /> コンポーネントに追加します。

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

テキストエリア フィールドでは、field オブジェクトをテキストエリアに展開します。

form.tsx
<Controller
  name="about"
  control={form.control}
  render={({ field, fieldState }) => (
    <Field data-invalid={fieldState.invalid}>
      <FieldLabel htmlFor="form-rhf-textarea-about">More about you</FieldLabel>
      <Textarea
        {...field}
        id="form-rhf-textarea-about"
        aria-invalid={fieldState.invalid}
        placeholder="I'm a software engineer..."
        className="min-h-[120px]"
      />
      <FieldDescription>
        Tell us more about yourself. This will be used to help us personalize
        your experience.
      </FieldDescription>
      {fieldState.invalid && <FieldError errors={[fieldState.error]} />}
    </Field>
  )}
/>

選択

  • コンポーネントを選択するには、<Select /> コンポーネントで field.value および field.onChange を使用します。
  • エラーを表示するには、aria-invalid プロパティを <SelectTrigger /> コンポーネントに追加し、data-invalid プロパティを <Field /> コンポーネントに追加します。

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

form.tsx
<Controller
  name="language"
  control={form.control}
  render={({ field, fieldState }) => (
    <Field orientation="responsive" data-invalid={fieldState.invalid}>
      <FieldContent>
        <FieldLabel htmlFor="form-rhf-select-language">
          Spoken Language
        </FieldLabel>
        <FieldDescription>
          For best results, select the language you speak.
        </FieldDescription>
        {fieldState.invalid && <FieldError errors={[fieldState.error]} />}
      </FieldContent>
      <Select
        name={field.name}
        value={field.value}
        onValueChange={field.onChange}
      >
        <SelectTrigger
          id="form-rhf-select-language"
          aria-invalid={fieldState.invalid}
          className="min-w-[120px]"
        >
          <SelectValue placeholder="Select" />
        </SelectTrigger>
        <SelectContent position="item-aligned">
          <SelectItem value="auto">Auto</SelectItem>
          <SelectItem value="en">English</SelectItem>
        </SelectContent>
      </Select>
    </Field>
  )}
/>

チェックボックス

  • チェックボックス配列の場合は、配列操作で field.value および field.onChange を使用します。
  • エラーを表示するには、aria-invalid プロパティを <Checkbox /> コンポーネントに追加し、data-invalid プロパティを <Field /> コンポーネントに追加します。
  • 適切なスタイルと間隔を確保するには、必ず data-slot="checkbox-group" を <FieldGroup /> コンポーネントに追加してください。

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

form.tsx
<Controller
  name="tasks"
  control={form.control}
  render={({ field, fieldState }) => (
    <FieldSet>
      <FieldLegend variant="label">Tasks</FieldLegend>
      <FieldDescription>
        Get notified when tasks you&apos;ve created have updates.
      </FieldDescription>
      <FieldGroup data-slot="checkbox-group">
        {tasks.map((task) => (
          <Field
            key={task.id}
            orientation="horizontal"
            data-invalid={fieldState.invalid}
          >
            <Checkbox
              id={`form-rhf-checkbox-${task.id}`}
              name={field.name}
              aria-invalid={fieldState.invalid}
              checked={field.value.includes(task.id)}
              onCheckedChange={(checked) => {
                const newValue = checked
                  ? [...field.value, task.id]
                  : field.value.filter((value) => value !== task.id)
                field.onChange(newValue)
              }}
            />
            <FieldLabel
              htmlFor={`form-rhf-checkbox-${task.id}`}
              className="font-normal"
            >
              {task.label}
            </FieldLabel>
          </Field>
        ))}
      </FieldGroup>
      {fieldState.invalid && <FieldError errors={[fieldState.error]} />}
    </FieldSet>
  )}
/>

ラジオ グループ

  • ラジオ グループの場合は、<RadioGroup /> コンポーネントで field.value および field.onChange を使用します。
  • エラーを表示するには、aria-invalid プロパティを <RadioGroupItem /> コンポーネントに追加し、data-invalid プロパティを <Field /> コンポーネントに追加します。

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

form.tsx
<Controller
  name="plan"
  control={form.control}
  render={({ field, fieldState }) => (
    <FieldSet>
      <FieldLegend>Plan</FieldLegend>
      <FieldDescription>
        You can upgrade or downgrade your plan at any time.
      </FieldDescription>
      <RadioGroup
        name={field.name}
        value={field.value}
        onValueChange={field.onChange}
      >
        {plans.map((plan) => (
          <FieldLabel key={plan.id} htmlFor={`form-rhf-radiogroup-${plan.id}`}>
            <Field orientation="horizontal" data-invalid={fieldState.invalid}>
              <FieldContent>
                <FieldTitle>{plan.title}</FieldTitle>
                <FieldDescription>{plan.description}</FieldDescription>
              </FieldContent>
              <RadioGroupItem
                value={plan.id}
                id={`form-rhf-radiogroup-${plan.id}`}
                aria-invalid={fieldState.invalid}
              />
            </Field>
          </FieldLabel>
        ))}
      </RadioGroup>
      {fieldState.invalid && <FieldError errors={[fieldState.error]} />}
    </FieldSet>
  )}
/>

スイッチ

  • スイッチでは、<Switch /> コンポーネントで field.value と field.onChange を使います。
  • エラーを表示するには、aria-invalid プロパティを <Switch /> コンポーネントに追加し、data-invalid プロパティを <Field /> コンポーネントに追加します。

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

form.tsx
<Controller
  name="twoFactor"
  control={form.control}
  render={({ field, fieldState }) => (
    <Field orientation="horizontal" data-invalid={fieldState.invalid}>
      <FieldContent>
        <FieldLabel htmlFor="form-rhf-switch-twoFactor">
          Multi-factor authentication
        </FieldLabel>
        <FieldDescription>
          Enable multi-factor authentication to secure your account.
        </FieldDescription>
        {fieldState.invalid && <FieldError errors={[fieldState.error]} />}
      </FieldContent>
      <Switch
        id="form-rhf-switch-twoFactor"
        name={field.name}
        checked={field.value}
        onCheckedChange={field.onChange}
        aria-invalid={fieldState.invalid}
      />
    </Field>
  )}
/>

複雑なフォーム

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

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

フォームをリセット

form.reset() を使用してフォームをデフォルト値にリセットします。

<Button type="button" variant="outline" onClick={() => form.reset()}>
  Reset
</Button>

配列フィールド

React Hook Form は、動的配列フィールドを管理するための useFieldArray フックを提供します。これは、フィールドを動的に追加または削除する必要がある場合に便利です。

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

useFieldArray を使う

useFieldArray フックを使用して、配列フィールドを管理します。 fields、append、および remove メソッドが提供されます。

form.tsx
import { useFieldArray, useForm } from "react-hook-form"
 
export function ExampleForm() {
  const form = useForm({
    // ... form config
  })
 
  const { fields, append, remove } = useFieldArray({
    control: form.control,
    name: "emails",
  })
}

配列フィールド構造

配列フィールドを <FieldLegend /> と <FieldDescription /> で <FieldSet /> でラップします。

form.tsx
<FieldSet className="gap-4">
  <FieldLegend variant="label">Email Addresses</FieldLegend>
  <FieldDescription>
    Add up to 5 email addresses where we can contact you.
  </FieldDescription>
  <FieldGroup className="gap-4">{/* Array items go here */}</FieldGroup>
</FieldSet>

配列項目のコントローラー パターン

fields 配列をマップし、各項目に <Controller /> を使います。キーには必ず field.id を使ってください。

form.tsx
{
  fields.map((field, index) => (
    <Controller
      key={field.id}
      name={`emails.${index}.address`}
      control={form.control}
      render={({ field: controllerField, fieldState }) => (
        <Field orientation="horizontal" data-invalid={fieldState.invalid}>
          <FieldContent>
            <InputGroup>
              <InputGroupInput
                {...controllerField}
                id={`form-rhf-array-email-${index}`}
                aria-invalid={fieldState.invalid}
                placeholder="name@example.com"
                type="email"
                autoComplete="email"
              />
              {/* Remove button */}
            </InputGroup>
            {fieldState.invalid && <FieldError errors={[fieldState.error]} />}
          </FieldContent>
        </Field>
      )}
    />
  ))
}

アイテムの追加

append メソッドを使用して、新しい項目を配列に追加します。

form.tsx
<Button
  type="button"
  variant="outline"
  size="sm"
  onClick={() => append({ address: "" })}
  disabled={fields.length >= 5}
>
  Add Email Address
</Button>

項目の削除

remove メソッドを使用して、配列から項目を削除します。条件付きで削除ボタンを追加します。

form.tsx
{
  fields.length > 1 && (
    <InputGroupAddon align="inline-end">
      <InputGroupButton
        type="button"
        variant="ghost"
        size="icon-xs"
        onClick={() => remove(index)}
        aria-label={`Remove email ${index + 1}`}
      >
        <XIcon />
      </InputGroupButton>
    </InputGroupAddon>
  )
}

配列の検証

Zod の array メソッドを使って配列フィールドを検証します。

form.tsx
const formSchema = z.object({
  emails: z
    .array(
      z.object({
        address: z.string().email("Enter a valid email address."),
      })
    )
    .min(1, "Add at least one email address.")
    .max(5, "You can add up to 5 email addresses."),
})