- Accordion
- Alert
- Alert Dialog
- Aspect Ratio
- Attachment
- Avatar
- Badge
- Breadcrumb
- Bubble
- Button
- Button Group
- Calendar
- Card
- Carousel
- Chart
- Checkbox
- Collapsible
- Combobox
- Command
- Context Menu
- Data Table
- Date Picker
- Dialog
- Direction
- Drawer
- Dropdown Menu
- Empty
- Field
- Hover Card
- Input
- Input Group
- Input OTP
- Item
- Kbd
- Label
- Marker
- Menubar
- Message
- Message Scroller
- Native Select
- Navigation Menu
- Pagination
- Popover
- Progress
- Questionnaire
- Radio Group
- Resizable
- Scroll Area
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Spinner
- Switch
- Table
- Tabs
- Textarea
- Toast
- Toggle
- Toggle Group
- Tooltip
- Typography
このガイドでは、React Hook Form を使用してフォームを構築する方法を見ていきます。 <Field /> コンポーネントを使用したフォームの構築、Zod を使用したスキーマ検証の追加、エラー処理、アクセシビリティなどについて説明します。
デモ#
次のフォームを構築します。単純なテキスト入力とテキストエリアがあります。送信時にフォーム データを検証し、エラーがあれば表示します。
注: このデモでは、React Hook Form でスキーマ検証とフォーム エラーがどのように機能するかを示すため、ブラウザー検証を意図的に無効にしています。 本番コードでは、基本的なブラウザー検証も追加することをおすすめします。
コンポーネント 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 スキーマを使用してフォームの形状を定義します。
注: この例では、スキーマ検証に zod v3 を使用していますが、 React Hook
Form では、スキーマを resolver に渡して検証します。
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 リゾルバーも追加します。
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 オプションに渡します。
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" | ブラー時に検証がトリガーされます。 |
"onSubmit" | 検証は送信時にトリガーされます (デフォルト)。 |
"onTouched" | 検証は最初のブラー時にトリガーされ、その後はすべての変更時にトリガーされます。 |
"all" | 検証はブラーと変更時にトリガーされます。 |
エラーの表示#
<FieldError /> を使用してフィールドの横にエラーを表示します。スタイルとアクセシビリティについて:
data-invalidプロパティを<Field />コンポーネントに追加します。<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>
)}
/>さまざまなフィールド タイプの操作#
入力#
- 入力フィールドの場合は、
fieldオブジェクトを<Input />コンポーネントに展開します。 - エラーを表示するには、
aria-invalidプロパティを<Input />コンポーネントに追加し、data-invalidプロパティを<Field />コンポーネントに追加します。
コンポーネント form-rhf-input が見つかりません。
単純なテキスト入力では、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>
)}
/>テキストエリア#
- テキストエリア フィールドの場合は、
fieldオブジェクトを<Textarea />コンポーネントに展開します。 - エラーを表示するには、
aria-invalidプロパティを<Textarea />コンポーネントに追加し、data-invalidプロパティを<Field />コンポーネントに追加します。
コンポーネント form-rhf-textarea が見つかりません。
テキストエリア フィールドでは、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 />コンポーネントでfield.valueおよびfield.onChangeを使用します。 - エラーを表示するには、
aria-invalidプロパティを<SelectTrigger />コンポーネントに追加し、data-invalidプロパティを<Field />コンポーネントに追加します。
コンポーネント form-rhf-select が見つかりません。
<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 が見つかりません。
<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>
)}
/>ラジオ グループ#
- ラジオ グループの場合は、
<RadioGroup />コンポーネントでfield.valueおよびfield.onChangeを使用します。 - エラーを表示するには、
aria-invalidプロパティを<RadioGroupItem />コンポーネントに追加し、data-invalidプロパティを<Field />コンポーネントに追加します。
コンポーネント form-rhf-radiogroup が見つかりません。
<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 が見つかりません。
<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 メソッドが提供されます。
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 /> でラップします。
<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 を使ってください。
{
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."),
})