在本指南中,我们将研究如何使用 React Hook Form 构建表单。我们将介绍如何使用 <Field /> 组件构建表单、使用 Zod 添加模式验证、错误处理、可访问性等内容。
🌐 In this guide, we will take a look at building forms with React Hook Form. We'll cover building forms with the <Field /> component, adding schema validation using Zod, error handling, accessibility, and more.
演示
🌐 Demo
我们将要创建以下表单。它有一个简单的文本输入框和一个多行文本框。提交时,我们将验证表单数据并显示任何错误。
🌐 We are going to build the following form. It has a simple text input and a textarea. On submit, we'll validate the form data and display any errors.
注意: 为了本演示的目的,我们故意禁用了浏览器验证,以展示 React Hook Form 中模式验证和表单错误是如何工作的。建议在生产代码中添加基本的浏览器验证。
"use client"
import * as React from "react"方法
🌐 Approach
这个表单利用 React Hook Form 来实现高性能、灵活的表单处理。我们将使用 <Field /> 组件来构建表单,这让你可以完全自由地控制标记和样式。
🌐 This form leverages React Hook Form for performant, flexible form handling. We'll build our form using the <Field /> component, which gives you complete flexibility over the markup and styling.
- 使用 React Hook Form 的
useForm钩子进行表单状态管理。 <Controller />组件用于受控输入。<Field />组件用于构建可访问的表单。- 使用 Zod 和
zodResolver的客户端验证。
剖析
🌐 Anatomy
这是一个使用 React Hook Form 的 <Controller /> 组件和 <Field /> 组件的表单的基本示例。
🌐 Here's a basic example of a form using the <Controller /> component from React Hook Form and the <Field /> component.
<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>
)}
/>表单
🌐 Form
创建表单模式
🌐 Create a form schema
我们将首先使用 Zod 模式来定义我们表单的形状
🌐 We'll start by defining the shape of our form using a Zod schema
注意: 这个示例使用 zod v3 进行模式验证,但你可以用 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."),
})设置表单
🌐 Setup the form
接下来,我们将使用 React Hook Form 的 useForm 钩子来创建我们的表单实例。我们还将添加 Zod 解析器来验证表单数据。
🌐 Next, we'll use the useForm hook from React Hook Form to create our form instance. We'll also add the Zod resolver to validate the form data.
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)}>
</form>
)
}建立表单
🌐 Build the form
我们现在可以使用 React Hook Form 的 <Controller /> 组件和 <Field /> 组件来构建表单。
🌐 We can now build the form using the <Controller /> component from React Hook Form and the <Field /> component.
"use client"
import * as React from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import { Controller, useForm } from "react-hook-form"
import { toast } from "sonner"
import * as z from "zod"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import {
InputGroup,
InputGroupAddon,
InputGroupText,
InputGroupTextarea,
} from "@/components/ui/input-group"
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>) {
toast("You submitted the following values:", {
description: (
<pre className="mt-2 w-[320px] overflow-x-auto rounded-md bg-code p-4 text-code-foreground">
<code>{JSON.stringify(data, null, 2)}</code>
</pre>
),
position: "bottom-right",
classNames: {
content: "flex flex-col gap-2",
},
style: {
"--border-radius": "calc(var(--radius) + 4px)",
} as React.CSSProperties,
})
}
return (
<Card className="w-full sm:max-w-md">
<CardHeader>
<CardTitle>Bug Report</CardTitle>
<CardDescription>
Help us improve by reporting bugs you encounter.
</CardDescription>
</CardHeader>
<CardContent>
<form id="form-rhf-demo" onSubmit={form.handleSubmit(onSubmit)}>
<FieldGroup>
<Controller
name="title"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor="form-rhf-demo-title">
Bug Title
</FieldLabel>
<Input
{...field}
id="form-rhf-demo-title"
aria-invalid={fieldState.invalid}
placeholder="Login button not working on mobile"
autoComplete="off"
/>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</Field>
)}
/>
<Controller
name="description"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor="form-rhf-demo-description">
Description
</FieldLabel>
<InputGroup>
<InputGroupTextarea
{...field}
id="form-rhf-demo-description"
placeholder="I'm having an issue with the login button on mobile."
rows={6}
className="min-h-24 resize-none"
aria-invalid={fieldState.invalid}
/>
<InputGroupAddon align="block-end">
<InputGroupText className="tabular-nums">
{field.value.length}/100 characters
</InputGroupText>
</InputGroupAddon>
</InputGroup>
<FieldDescription>
Include steps to reproduce, expected behavior, and what
actually happened.
</FieldDescription>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</Field>
)}
/>
</FieldGroup>
</form>
</CardContent>
<CardFooter>
<Field orientation="horizontal">
<Button type="button" variant="outline" onClick={() => form.reset()}>
Reset
</Button>
<Button type="submit" form="form-rhf-demo">
Submit
</Button>
</Field>
</CardFooter>
</Card>
)
}
完成
🌐 Done
就是这样。你现在有一个完全可访问的表格,并带有客户端验证。
🌐 That's it. You now have a fully accessible form with client-side validation.
当你提交表单时,onSubmit 函数将使用验证过的表单数据被调用。如果表单数据无效,React Hook Form 会在每个字段旁显示错误信息。
🌐 When you submit the form, the onSubmit function will be called with the validated form data. If the form data is invalid, React Hook Form will display the errors next to each field.
验证
🌐 Validation
客户端验证
🌐 Client-side Validation
React Hook Form 使用 Zod 模式验证你的表单数据。定义一个模式并将其传递给 useForm 钩子的 resolver 选项。
🌐 React Hook Form validates your form data using the Zod schema. Define a schema and pass it to the resolver option of the useForm hook.
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: "",
},
})
}验证模式
🌐 Validation Modes
React Hook Form 支持不同的验证模式。
🌐 React Hook Form supports different validation modes.
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
mode: "onChange",
})| 模式 | 描述 |
|---|---|
"onChange" | 每次更改时触发验证。 |
"onBlur" | 失去焦点时触发验证。 |
"onSubmit" | 提交时触发验证(默认)。 |
"onTouched" | 首次失去焦点后触发验证,然后每次更改时触发验证。 |
"all" | 失去焦点和更改时触发验证。 |
显示错误
🌐 Displaying Errors
使用 <FieldError /> 在字段旁显示错误。用于样式和可访问性:
🌐 Display errors next to the field using <FieldError />. For styling and accessibility:
- 将
data-invalid属性添加到<Field />组件中。 - 将
aria-invalid属性添加到表单控件中,例如<Input />、<SelectTrigger />、<Checkbox />等。
<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>
)}
/>处理不同的字段类型
🌐 Working with Different Field Types
输入
🌐 Input
- 对于输入字段,将
field对象展开到<Input />组件上。 - 要显示错误,请在
<Input />组件中添加aria-invalid属性,在<Field />组件中添加data-invalid属性。
"use client"
import { zodResolver } from "@hookform/resolvers/zod"对于简单的文本输入,将 field 对象扩展到输入中。
🌐 For simple text inputs, spread the field object onto the input.
<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属性。
"use client"
import * as React from "react"对于文本区域字段,将 field 对象展开到文本区域上。
🌐 For textarea fields, spread the field object onto the textarea.
<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 />组件上使用field.value和field.onChange。 - 要显示错误,请在
<SelectTrigger />组件中添加aria-invalid属性,在<Field />组件中添加data-invalid属性。
"use client"
import * as React from "react"<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属性。 - 记得将
data-slot="checkbox-group"添加到<FieldGroup />组件中,以获得正确的样式和间距。
"use client"
import * as React from "react"<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属性。
"use client"
import * as React from "react"<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属性。
"use client"
import * as React from "react"<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
这是一个包含多个字段和验证的更复杂表单的示例。
🌐 Here is an example of a more complex form with multiple fields and validation.
"use client"
import * as React from "react"重置表单
🌐 Resetting the Form
使用 form.reset() 将表单重置为默认值。
🌐 Use form.reset() to reset the form to its default values.
<Button type="button" variant="outline" onClick={() => form.reset()}>
Reset
</Button>数组字段
🌐 Array Fields
React Hook Form 提供了一个 useFieldArray 钩子用于管理动态数组字段。当你需要动态添加或移除字段时,这非常有用。
🌐 React Hook Form provides a useFieldArray hook for managing dynamic array fields. This is useful when you need to add or remove fields dynamically.
"use client"
import * as React from "react"使用 useFieldArray
🌐 Using useFieldArray
使用 useFieldArray 钩子来管理数组字段。它提供了 fields、append 和 remove 方法。
🌐 Use the useFieldArray hook to manage array fields. It provides fields, append, and remove methods.
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",
})
}数组字段结构
🌐 Array Field Structure
将你的数组字段用一个 <FieldSet /> 封装,并搭配一个 <FieldLegend /> 和 <FieldDescription />。
🌐 Wrap your array fields in a <FieldSet /> with a <FieldLegend /> and <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"></FieldGroup>
</FieldSet>数组项的控制器模式
🌐 Controller Pattern for Array Items
对 fields 数组进行映射,并对每个项目使用 <Controller />。确保使用 field.id 作为 key。
🌐 Map over the fields array and use <Controller /> for each item. Make sure to use field.id as the key.
{
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"
/>
</InputGroup>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</FieldContent>
</Field>
)}
/>
))
}添加项目
🌐 Adding Items
使用 append 方法向数组中添加新项。
🌐 Use the append method to add new items to the array.
<Button
type="button"
variant="outline"
size="sm"
onClick={() => append({ address: "" })}
disabled={fields.length >= 5}
>
Add Email Address
</Button>移除条目
🌐 Removing Items
使用 remove 方法从数组中移除项。有条件地添加删除按钮。
🌐 Use the remove method to remove items from the array. Add the remove button conditionally.
{
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>
)
}数组验证
🌐 Array Validation
使用 Zod 的 array 方法来验证数组字段。
🌐 Use Zod's array method to validate array fields.
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."),
})