このガイドでは React Hook Form を使ったフォームの構築方法を説明します。<Field /> コンポーネントでのフォーム作成、Zod を用いたスキーマ検証、エラーハンドリング、アクセシビリティなどをカバーします。
デモ#
以下のフォームを作成します。シンプルなテキスト入力とテキストエリアを備えたフォームです。送信時にフォームデータを検証し、エラーがあれば表示します。
注意: このデモでは、React Hook Form のスキーマ検証とフォームエラーの動作を示すため、意図的にブラウザのバリデーションを無効化しています。本番環境では基本的なブラウザバリデーションを追加することをお勧めします。
Component form-rhf-demo not found in registry.
アプローチ#
このフォームは React Hook Form を使ってパフォーマンスと柔軟性を実現しています。<Field /> コンポーネントを使ってフォームを構築し、マークアップとスタイリングに完全な自由度を確保します。
- React Hook Form の
useFormフックでフォーム状態を管理。 - 制御インプットに
<Controller />コンポーネントを使用。 <Field />コンポーネントでアクセシブルなフォームを構築。zodResolverで Zod によるクライアントサイド検証。
属成#
<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 スキーマでフォームの形式を定義します。
注意: この例ではスキーマ検証に zod v3 を使用していますが、React Hook Form がサポートする他の Standard Schema 対応ライブラリに置き換えて使用できます。
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."),
})フォームのセットアップ#
useForm フックでフォームインスタンスを作成し、Zod リゾルバーで検証を追加します。
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>
)
}フォームの構築#
<Controller /> と <Field /> コンポーネントでフォームを作成できます。
完成#
以上です。クライアントサイド検証付きの完全なアクセシブルなフォームができました。
フォームを送信すると、検証済みデータで onSubmit が呼び出されます。入力が無効な場合、React Hook Form が各フィールドの横にエラーを表示します。
検証#
クライアントサイド検証#
React Hook Form は Zod スキーマでフォームデータを検証します。スキーマを定義し、useForm の resolver オプションに渡します。
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 は複数の検証モードをサポートしています。
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
mode: "onChange",
})| モード | 説明 |
|---|---|
"onChange" | 変更ごとに検証が実行されます。 |
"onBlur" | blur 時に検証が実行されます。 |
"onSubmit" | 送信時に検証が実行されます(デフォルト)。 |
"onTouched" | 初回 blur 時、その後は変更ごとに検証が実行されます。 |
"all" | blur と変更の両方で検証が実行されます。 |
エラーの表示#
<FieldError /> でフィールドの横にエラーを表示します。スタイリングとアクセシビリティのため:
<Field />コンポーネントにdata-invalidを付けます。<Input />、<SelectTrigger />、<Checkbox />などのフォームコントロールにaria-invalidを付けます。
<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>
)}
/>フィールドタイプ別の使い方#
Input#
- 入力フィールドには
fieldオブジェクトを<Input />にスプレッドします。 - エラー表示には
<Input />にaria-invalid、<Field />にdata-invalidを付けます。
Component form-rhf-input not found in registry.
シンプルなテキスト入力には、field オブジェクトを入力にスプレッドします。
<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>
)}
/>Textarea#
- テキストエリアフィールドには
fieldオブジェクトを<Textarea />にスプレッドします。 - エラー表示には
<Textarea />にaria-invalid、<Field />にdata-invalidを付けます。
Component form-rhf-textarea not found in registry.
テキストエリアフィールドには、field オブジェクトをテキストエリアにスプレッドします。
<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#
- Select コンポーネントには
<Select />にfield.valueとfield.onChangeを使用します。 - エラー表示には
<SelectTrigger />にaria-invalid、<Field />にdata-invalidを付けます。
Component form-rhf-select not found in registry.
<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>
)}
/>Checkbox#
- チェックボックスの配列には
field.valueとfield.onChangeに配列操作を使用します。 - エラー表示には
<Checkbox />にaria-invalid、<Field />にdata-invalidを付けます。 - 適切なスタイリングと間距のため、
<FieldGroup />にdata-slot="checkbox-group"を付けてください。
Component form-rhf-checkbox not found in registry.
<Controller
name="tasks"
control={form.control}
render={({ field, fieldState }) => (
<FieldSet>
<FieldLegend variant="label">Tasks</FieldLegend>
<FieldDescription>
Get notified when tasks you'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>
)}
/>Radio Group#
- ラジオグループには
<RadioGroup />にfield.valueとfield.onChangeを使用します。 - エラー表示には
<RadioGroupItem />にaria-invalid、<Field />にdata-invalidを付けます。
Component form-rhf-radiogroup not found in registry.
<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#
- スイッチには
<Switch />にfield.valueとfield.onChangeを使用します。 - エラー表示には
<Switch />にaria-invalid、<Field />にdata-invalidを付けます。
Component form-rhf-switch not found in registry.
<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>
)}
/>Complex Forms#
複数のフィールドと検証を持つ、より複雑なフォームの例。
Component form-rhf-complex not found in registry.
フォームのリセット#
form.reset() でフォームをデフォルト値にリセットします。
<Button type="button" variant="outline" onClick={() => form.reset()}>
Reset
</Button>配列フィールド#
React Hook Form は動的な配列フィールドを管理する useFieldArray フックを提供しています。フィールドの動的な追加・削除に便利です。
Component form-rhf-array not found in registry.
useFieldArray の使い方#
useFieldArray フックで配列フィールドを管理します。fields、append、remove メソッドが提供されます。
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",
})
}配列フィールドの構造#
配列フィールドは <FieldSet /> で包み、<FieldLegend /> と <FieldDescription /> を付けます。
<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>配列アイテムの Controller パターン#
fields 配列をマップして各アイテムに <Controller /> を使用します。キーには field.id を必ず使用してください。
{
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 メソッドで配列に新しいアイテムを追加します。
<Button
type="button"
variant="outline"
size="sm"
onClick={() => append({ address: "" })}
disabled={fields.length >= 5}
>
Add Email Address
</Button>アイテムの削除#
remove メソッドで配列からアイテムを削除します。条件付きで削除ボタンを追加します。
{
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 メソッドで配列フィールドを検証します。
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."),
})