در این راهنما ساخت فرم با React Hook Form را بررسی میکنیم؛ از ساخت فرم با کامپوننت <Field /> گرفته تا اعتبارسنجی Schema با Zod، مدیریت خطا، دسترسپذیری و موارد دیگر.
در این Demo یک فرم ساده با یک ورودی متنی و یک Textarea میسازیم. هنگام ارسال فرم، دادهها اعتبارسنجی میشوند و خطاهای احتمالی نمایش داده خواهند شد.
نکته: برای این Demo، اعتبارسنجی پیشفرض مرورگر را عمداً غیرفعال کردهایم تا نحوهٔ کار Schema Validation و مدیریت خطا در React Hook Form مشخص باشد. در پروژهٔ واقعی بهتر است اعتبارسنجی پایهٔ مرورگر را نیز فعال نگه دارید.
این فرم برای مدیریت سریع و انعطافپذیر وضعیت فرم از React Hook Form استفاده میکند. فرم را با کامپوننت <Field /> میسازیم تا کنترل کاملی روی Markup و Styling داشته باشیم.
useForm در React Hook Form برای مدیریت وضعیت فرم.<Controller /> برای ورودیهای Controlled.<Field /> برای ساخت فرمهای دسترسپذیر.zodResolver.در مثال پایه، از <Controller /> در React Hook Form و <Field /> برای ساخت فرم استفاده میکنیم:
<Controller
name="title"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor={field.name}>عنوان باگ</FieldLabel>
<Input
{...field}
id={field.name}
aria-invalid={fieldState.invalid}
placeholder="دکمهٔ ورود در موبایل کار نمیکند"
autoComplete="off"
/>
<FieldDescription>
یک عنوان کوتاه برای گزارش باگ بنویسید.
</FieldDescription>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</Field>
)}
/>ابتدا ساختار فرم را با یک Schema از Zod تعریف میکنیم.
نکته: این مثال از zod v3 برای اعتبارسنجی Schema استفاده میکند، اما
میتوانید از هر کتابخانهٔ اعتبارسنجی Standard Schema که React Hook Form از آن
پشتیبانی میکند نیز استفاده کنید.
import * as z from "zod"
const formSchema = z.object({
title: z
.string()
.min(5, "عنوان باگ باید حداقل ۵ کاراکتر باشد.")
.max(32, "عنوان باگ باید حداکثر ۳۲ کاراکتر باشد."),
description: z
.string()
.min(20, "توضیحات باید حداقل ۲۰ کاراکتر باشد.")
.max(100, "توضیحات باید حداکثر ۱۰۰ کاراکتر باشد."),
})سپس با استفاده از Hook useForm در React Hook Form، Instance فرم را ایجاد میکنیم و Resolver مربوط به 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, "عنوان باگ باید حداقل ۵ کاراکتر باشد.")
.max(32, "عنوان باگ باید حداکثر ۳۲ کاراکتر باشد."),
description: z
.string()
.min(20, "توضیحات باید حداقل ۲۰ کاراکتر باشد.")
.max(100, "توضیحات باید حداکثر ۱۰۰ کاراکتر باشد."),
})
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 /> در React Hook Form و کامپوننت <Field /> بسازیم.
همین! حالا یک فرم کاملاً دسترسپذیر با اعتبارسنجی سمت Client دارید.
هنگام ارسال فرم، تابع onSubmit با دادههای اعتبارسنجیشدهٔ فرم اجرا میشود. اگر دادهها معتبر نباشند، React Hook Form خطاها را در کنار فیلدهای مربوطه نمایش میدهد.
React Hook Form دادههای فرم را با استفاده از Schema مربوط به Zod اعتبارسنجی میکند. Schema را تعریف کنید و آن را به Option مربوط به resolver در Hook useForm بدهید.
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",
})برای نمایش خطا در کنار فیلد از <FieldError /> استفاده کنید.
برای Styling و دسترسپذیری:
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>
)}
/>field را با {...field} روی <Input /> اعمال کنید.aria-invalid را به <Input /> و Prop data-invalid را به <Field /> اضافه کنید.برای ورودیهای متنی ساده، آبجکت field را مستقیماً با {...field} روی <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>
)}
/>field را با {...field} روی <Textarea /> اعمال کنید.aria-invalid را به <Textarea /> و Prop data-invalid را به <Field /> اضافه کنید.برای فیلدهای چندخطی، آبجکت field را با {...field} روی <Textarea /> اعمال کنید.
<Controller
name="about"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor="form-rhf-textarea-about">دربارهٔ شما</FieldLabel>
<Textarea
{...field}
id="form-rhf-textarea-about"
aria-invalid={fieldState.invalid}
placeholder="من مهندس نرمافزار هستم..."
className="min-h-16"
/>
<FieldDescription>
اطلاعات بیشتری دربارهٔ خودتان بنویسید. از این اطلاعات برای شخصیسازی
تجربهٔ شما استفاده خواهد شد.
</FieldDescription>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</Field>
)}
/>field.value و field.onChange روی کامپوننت <Select /> استفاده کنید.aria-invalid را به <SelectTrigger /> و Prop data-invalid را به <Field /> اضافه کنید.<Controller
name="language"
control={form.control}
render={({ field, fieldState }) => (
<Field orientation="responsive" data-invalid={fieldState.invalid}>
<FieldContent>
<FieldLabel htmlFor="form-rhf-select-language">
زبان گفتاری
</FieldLabel>
<FieldDescription>
برای نتیجهٔ بهتر، زبانی را که صحبت میکنید انتخاب کنید.
</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="انتخاب کنید" />
</SelectTrigger>
<SelectContent position="item-aligned">
<SelectItem value="auto">خودکار</SelectItem>
<SelectItem value="fa">فارسی</SelectItem>
</SelectContent>
</Select>
</Field>
)}
/>field.value و field.onChange همراه با مدیریت آرایه استفاده کنید.aria-invalid را به <Checkbox /> و Prop data-invalid را به <Field /> اضافه کنید.data-slot="checkbox-group" را به <FieldGroup /> اضافه کنید.<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>
)}
/>field.value و field.onChange روی کامپوننت <RadioGroup /> استفاده کنید.aria-invalid را به <RadioGroupItem /> و Prop data-invalid را به <Field /> اضافه کنید.<Controller
name="plan"
control={form.control}
render={({ field, fieldState }) => (
<FieldSet>
<FieldLegend>پلن</FieldLegend>
<FieldDescription>
هر زمان بخواهید میتوانید پلن خود را ارتقا دهید یا به پلن پایینتر
برگردید.
</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>
)}
/>field.value و field.onChange روی کامپوننت <Switch /> استفاده کنید.aria-invalid را به <Switch /> و Prop data-invalid را به <Field /> اضافه کنید.<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.reset() استفاده کنید.
<Button type="button" variant="outline" onClick={() => form.reset()}>
بازنشانی
</Button>React Hook Form برای مدیریت فیلدهای آرایهای پویا، Hook useFieldArray را ارائه میدهد. زمانی که نیاز دارید فیلدها را بهصورت پویا اضافه یا حذف کنید، میتوانید از آن استفاده کنید.
useFieldArrayبرای مدیریت فیلدهای آرایهای از Hook useFieldArray استفاده کنید. این Hook متدهای 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 را بهعنوان 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="[email protected]"
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>
)
}برای اعتبارسنجی فیلدهای آرایهای از متد array در Zod استفاده کنید.
const formSchema = z.object({
emails: z
.array(
z.object({
address: z.string().email("یک آدرس ایمیل معتبر وارد کنید."),
})
)
.min(1, "حداقل یک آدرس ایمیل اضافه کنید.")
.max(5, "میتوانید حداکثر ۵ آدرس ایمیل اضافه کنید."),
})