React Hook Form چیست؟ آموزش کامل ساخت و اعتبارسنجی فرم در React و Next.js
در این آموزش React Hook Form را از صفر یاد میگیرید؛ از ساخت و اعتبارسنجی فرم با TypeScript و Zod تا مدیریت خطا، فیلدهای پویا، اتصال به Next.js و ساخت فرم عملی برای برنامه هوش مصنوعی.
ساخت فرم در React در نگاه اول ساده به نظر میرسد؛ اما وقتی اعتبارسنجی، نمایش خطا، مقدار اولیه، فیلدهای پویا، وضعیت Loading، ارسال Async و هماهنگی با کامپوننتهای رابط کاربری به پروژه اضافه شوند، مدیریت فرم میتواند پیچیده شود.
React Hook Form یک کتابخانه محبوب برای مدیریت فرم و اعتبارسنجی ورودیها در React است. این کتابخانه به شما اجازه میدهد فرمهای کوچک و بزرگ را با کد کمتر، Type Safety مناسب و کنترل دقیق وضعیت فرم پیادهسازی کنید.
در این آموزش، React Hook Form را از نصب اولیه تا ساخت یک فرم واقعی متصل به API بررسی میکنیم. همچنین با استفاده از TypeScript و Zod، اعتبارسنجی قابلاستفاده در Runtime ایجاد خواهیم کرد.
React Hook Form چیست؟
React Hook Form یک کتابخانه مدیریت فرم برای React و React Native است. مهمترین Hook آن useForm نام دارد و قابلیتهایی مانند موارد زیر را فراهم میکند:
- ثبت و مدیریت فیلدها
- اعتبارسنجی اطلاعات
- نمایش خطاهای هر فیلد
- مدیریت Submit
- تعیین مقدار اولیه
- مشاهده تغییر مقادیر
- پاکسازی یا Reset فرم
- مدیریت فیلدهای پویا
- اتصال به کتابخانههای اعتبارسنجی
- مدیریت کامپوننتهای Controlled
- بررسی وضعیت Dirty، Touched و Valid
- مدیریت خطاهای Backend
براساس وبسایت رسمی React Hook Form، این کتابخانه تلاش میکند تعداد Re-renderهای غیرضروری را کاهش دهد و از قابلیتهای استاندارد فرم HTML استفاده کند.
چرا مدیریت فرم در React دشوار میشود؟
در فرم ساده React ممکن است برای هر فیلد یک State تعریف کنیم:
'use client'
import { useState } from 'react'
export function ContactForm() {
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [message, setMessage] = useState('')
const [errors, setErrors] = useState({
name: '',
email: '',
message: ''
})
return (
<form>
<input
value={name}
onChange={(event) =>
setName(event.target.value)
}
/>
<input
value={email}
onChange={(event) =>
setEmail(event.target.value)
}
/>
<textarea
value={message}
onChange={(event) =>
setMessage(event.target.value)
}
/>
</form>
)
}
با افزایش تعداد فیلدها باید موارد زیر را نیز مدیریت کنیم:
- Validation
- پیام خطا
- Submit
- Loading
- مقدار اولیه
- Reset
- خطای سرور
- فیلدهای شرطی
- فیلدهای Array
- File Input
- جلوگیری از Submit تکراری
- تبدیل نوع داده
React Hook Form این منطق تکراری را در قالب API یکپارچه مدیریت میکند.
نصب React Hook Form
در پروژه React، Vite یا Next.js دستور زیر را اجرا کنید:
npm install react-hook-form
اگر قصد دارید از Zod نیز استفاده کنید:
npm install zod @hookform/resolvers
پکیج @hookform/resolvers امکان اتصال React Hook Form به کتابخانههایی مانند Zod، Yup، Joi، Valibot و Ajv را فراهم میکند. فهرست Resolverهای پشتیبانیشده در Repository رسمی React Hook Form Resolvers موجود است.
ساخت اولین فرم با useForm
'use client'
import { useForm } from 'react-hook-form'
type LoginFormValues = {
email: string
password: string
}
export function LoginForm() {
const {
register,
handleSubmit
} = useForm<LoginFormValues>()
function onSubmit(data: LoginFormValues) {
console.log(data)
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label htmlFor="email">
ایمیل
</label>
<input
id="email"
type="email"
{...register('email')}
/>
<label htmlFor="password">
رمز عبور
</label>
<input
id="password"
type="password"
{...register('password')}
/>
<button type="submit">
ورود
</button>
</form>
)
}
در این مثال:
useFormوضعیت فرم را مدیریت میکند.registerفیلد را در فرم ثبت میکند.handleSubmitقبل از اجرای تابع Submit، اعتبار فرم را بررسی میکند.dataشامل مقادیر نهایی فرم است.- Generic مربوط به
useFormنوع تمام فیلدها را برای TypeScript مشخص میکند.
register چگونه کار میکند؟
تابع register مشخصات لازم برای اتصال Input به فرم را برمیگرداند:
<input {...register('email')} />
این عبارت در عمل ویژگیهایی مانند موارد زیر را به Input متصل میکند:
namerefonChangeonBlur
میتوان قوانین اعتبارسنجی را نیز داخل register تعریف کرد:
<input
type="email"
{...register('email', {
required: 'ایمیل الزامی است',
pattern: {
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: 'فرمت ایمیل معتبر نیست'
}
})}
/>
نمایش خطاهای فرم
خطاها داخل formState.errors قرار میگیرند:
'use client'
import { useForm } from 'react-hook-form'
type RegisterFormValues = {
name: string
email: string
password: string
}
export function RegisterForm() {
const {
register,
handleSubmit,
formState: {
errors
}
} = useForm<RegisterFormValues>()
function onSubmit(data: RegisterFormValues) {
console.log(data)
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="name">
نام
</label>
<input
id="name"
{...register('name', {
required: 'نام الزامی است',
minLength: {
value: 2,
message:
'نام باید حداقل دو کاراکتر داشته باشد'
}
})}
aria-invalid={Boolean(errors.name)}
aria-describedby={
errors.name ? 'name-error' : undefined
}
/>
{errors.name ? (
<p id="name-error" role="alert">
{errors.name.message}
</p>
) : null}
</div>
<div>
<label htmlFor="email">
ایمیل
</label>
<input
id="email"
type="email"
{...register('email', {
required: 'ایمیل الزامی است',
pattern: {
value:
/^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: 'ایمیل معتبر نیست'
}
})}
aria-invalid={Boolean(errors.email)}
/>
{errors.email ? (
<p role="alert">
{errors.email.message}
</p>
) : null}
</div>
<div>
<label htmlFor="password">
رمز عبور
</label>
<input
id="password"
type="password"
{...register('password', {
required: 'رمز عبور الزامی است',
minLength: {
value: 8,
message:
'رمز عبور باید حداقل هشت کاراکتر باشد'
}
})}
aria-invalid={Boolean(errors.password)}
/>
{errors.password ? (
<p role="alert">
{errors.password.message}
</p>
) : null}
</div>
<button type="submit">
ثبتنام
</button>
</form>
)
}
استفاده از role="alert" و aria-invalid دسترسپذیری پیامهای خطا را بهتر میکند.
قوانین اعتبارسنجی داخلی
React Hook Form تعدادی قانون Validation داخلی ارائه میدهد:
required
register('title', {
required: 'عنوان الزامی است'
})
minLength و maxLength
register('description', {
minLength: {
value: 20,
message:
'توضیحات باید حداقل ۲۰ کاراکتر باشد'
},
maxLength: {
value: 1000,
message:
'توضیحات نمیتواند بیشتر از ۱۰۰۰ کاراکتر باشد'
}
})
min و max
register('temperature', {
valueAsNumber: true,
min: {
value: 0,
message: 'کمترین مقدار صفر است'
},
max: {
value: 2,
message: 'بیشترین مقدار دو است'
}
})
pattern
register('username', {
pattern: {
value: /^[a-zA-Z0-9_]+$/,
message:
'نام کاربری فقط میتواند شامل حروف انگلیسی، عدد و زیرخط باشد'
}
})
validate
برای Validation سفارشی:
register('prompt', {
validate: {
notOnlySpaces: (value) =>
value.trim().length > 0 ||
'متن نمیتواند فقط شامل فاصله باشد',
enoughWords: (value) =>
value.trim().split(/\s+/).length >= 3 ||
'حداقل سه کلمه وارد کنید'
}
})
تبدیل نوع داده ورودی
مقدار Inputهای HTML معمولاً String است؛ حتی اگر type="number" داشته باشند.
برای تبدیل عدد:
<input
type="number"
{...register('maxTokens', {
valueAsNumber: true
})}
/>
برای تاریخ:
<input
type="date"
{...register('publishDate', {
valueAsDate: true
})}
/>
برای تبدیل سفارشی:
<input
{...register('title', {
setValueAs: (value) =>
typeof value === 'string'
? value.trim()
: value
})}
/>
دقت کنید setValueAs روی داده ورودی اثر میگذارد. اگر میخواهید نسخه اصلی متن کاربر حفظ شود، Trim کردن را هنگام Submit یا در Schema انجام دهید.
defaultValues چیست؟
برای تعیین مقادیر اولیه از defaultValues استفاده کنید:
const form = useForm<SettingsFormValues>({
defaultValues: {
model: '',
temperature: 0.3,
maxTokens: 1000,
stream: true
}
})
مقادیر اولیه را ترجیحاً در یک محل تعریف کنید:
const defaultValues: SettingsFormValues = {
model: '',
temperature: 0.3,
maxTokens: 1000,
stream: true
}
const form = useForm<SettingsFormValues>({
defaultValues
})
این کار Reset کردن فرم را نیز سادهتر میکند.
formState شامل چه اطلاعاتی است؟
formState وضعیتهای مهمی ارائه میدهد:
const {
formState: {
errors,
isDirty,
dirtyFields,
touchedFields,
isValid,
isSubmitting,
isSubmitted,
isSubmitSuccessful,
submitCount
}
} = useForm<FormValues>()
کاربرد آنها:
| مقدار | کاربرد |
|---|---|
errors | خطاهای Validation |
isDirty | آیا حداقل یک مقدار تغییر کرده است؟ |
dirtyFields | کدام فیلدها تغییر کردهاند؟ |
touchedFields | کاربر کدام فیلدها را لمس کرده است؟ |
isValid | آیا فرم معتبر است؟ |
isSubmitting | آیا Submit در حال اجراست؟ |
isSubmitted | آیا فرم حداقل یک بار Submit شده است؟ |
isSubmitSuccessful | آیا Submit بدون خطای داخلی تمام شده است؟ |
submitCount | تعداد دفعات Submit |
مثال جلوگیری از Submit تکراری:
<button
type="submit"
disabled={isSubmitting}
>
{isSubmitting
? 'در حال ارسال...'
: 'ارسال'}
</button>
Validation Mode چیست؟
با گزینه mode مشخص میکنید Validation چه زمانی اجرا شود:
useForm<FormValues>({
mode: 'onSubmit'
})
حالتهای متداول:
onSubmit: هنگام SubmitonBlur: هنگام خروج کاربر از فیلدonChange: با هر تغییرonTouched: پس از اولین Blurall: ترکیبی از Blur و Change
برای بسیاری از فرمها، onSubmit یا onTouched انتخاب متعادلی است. استفاده از onChange در فرمهای بزرگ میتواند Validation را بیش از حد تکرار کند.
مثال:
const form = useForm<FormValues>({
mode: 'onTouched',
reValidateMode: 'onChange'
})
در این حالت خطای اولیه پس از لمس فیلد نمایش داده میشود و پس از آن با تغییر مقدار بروزرسانی خواهد شد.
اعتبارسنجی فرم با Zod
Validationهای داخل register برای فرمهای ساده مناسباند. در فرمهای بزرگتر بهتر است قوانین در یک Schema مستقل تعریف شوند.
// src/schemas/assistant-form.ts
import { z } from 'zod'
export const assistantFormSchema = z.object({
prompt: z
.string()
.trim()
.min(1, 'متن درخواست الزامی است')
.min(
10,
'متن درخواست باید حداقل ۱۰ کاراکتر باشد'
)
.max(
4000,
'متن درخواست نمیتواند بیشتر از ۴۰۰۰ کاراکتر باشد'
),
model: z
.string()
.trim()
.min(1, 'انتخاب مدل الزامی است'),
temperature: z
.number()
.min(0, 'Temperature نمیتواند کمتر از صفر باشد')
.max(2, 'Temperature نمیتواند بیشتر از دو باشد'),
includeContext: z.boolean()
})
export type AssistantFormInput = z.input<
typeof assistantFormSchema
>
export type AssistantFormOutput = z.output<
typeof assistantFormSchema
>
اتصال Schema به React Hook Form:
'use client'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import {
assistantFormSchema,
type AssistantFormInput,
type AssistantFormOutput
} from '@/schemas/assistant-form'
export function AssistantForm() {
const {
register,
handleSubmit,
formState: {
errors
}
} = useForm<
AssistantFormInput,
unknown,
AssistantFormOutput
>({
resolver: zodResolver(assistantFormSchema),
defaultValues: {
prompt: '',
model: '',
temperature: 0.3,
includeContext: false
}
})
async function onSubmit(
data: AssistantFormOutput
) {
console.log(data)
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<textarea
{...register('prompt')}
/>
{errors.prompt ? (
<p role="alert">
{errors.prompt.message}
</p>
) : null}
<select {...register('model')}>
<option value="">
انتخاب مدل
</option>
<option value="YOUR_MODEL_ID">
مدل انتخابی
</option>
</select>
{errors.model ? (
<p role="alert">
{errors.model.message}
</p>
) : null}
<input
type="number"
step="0.1"
{...register('temperature', {
valueAsNumber: true
})}
/>
{errors.temperature ? (
<p role="alert">
{errors.temperature.message}
</p>
) : null}
<label>
<input
type="checkbox"
{...register('includeContext')}
/>
استفاده از Context
</label>
<button type="submit">
ارسال
</button>
</form>
)
}
Resolver رسمی میتواند در بسیاری از موارد Type خروجی را از Schema استنتاج کند. در مثال بالا Type ورودی و خروجی بهصورت صریح تعریف شدهاند تا تبدیلهای احتمالی Zod نیز قابلکنترل باشند.
آیا Validation سمت Client کافی است؟
خیر. Validation مرورگر فقط تجربه کاربری را بهتر میکند. کاربر یا برنامه دیگر میتواند مستقیماً Endpoint را فراخوانی کرده و فرم Client را دور بزند.
Schema باید در سرور نیز اجرا شود:
// src/app/api/assistant/route.ts
import { assistantFormSchema } from '@/schemas/assistant-form'
export async function POST(request: Request) {
const body: unknown = await request.json()
const result =
assistantFormSchema.safeParse(body)
if (!result.success) {
return Response.json(
{
error: 'اطلاعات واردشده معتبر نیست',
fields: result.error.flatten().fieldErrors
},
{
status: 400
}
)
}
const data = result.data
return Response.json({
success: true,
data
})
}
Validation Client برای UX است و Validation Server برای صحت و یکپارچگی داده ضروری است.
ارسال Async فرم
async function onSubmit(
data: AssistantFormOutput
) {
setServerError('')
setAnswer('')
try {
const response = await fetch('/api/assistant', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
const result = (await response.json()) as {
answer?: string
error?: string
}
if (!response.ok) {
throw new Error(
result.error || 'ارسال فرم ناموفق بود'
)
}
if (!result.answer) {
throw new Error('پاسخ معتبری دریافت نشد')
}
setAnswer(result.answer)
} catch (error) {
setServerError(
error instanceof Error
? error.message
: 'خطای پیشبینینشده رخ داد'
)
}
}
خود handleSubmit از تابع Async پشتیبانی میکند و isSubmitting را هنگام اجرای Promise بروزرسانی میکند:
<button
type="submit"
disabled={isSubmitting}
>
{isSubmitting
? 'در حال پردازش...'
: 'ارسال درخواست'}
</button>
مدیریت خطاهای Backend با setError
اگر Backend خطای مربوط به یک فیلد برگرداند، میتوان آن را با setError نمایش داد:
const {
setError
} = useForm<FormValues>()
مثال:
if (response.status === 409) {
setError('email', {
type: 'server',
message:
'قبلاً حسابی با این ایمیل ثبت شده است'
})
return
}
برای خطای کلی فرم:
setError('root.server', {
type: 'server',
message:
'ارسال اطلاعات ناموفق بود؛ دوباره تلاش کنید'
})
نمایش:
{errors.root?.server ? (
<p role="alert">
{errors.root.server.message}
</p>
) : null}
خطای Server را به فیلد مرتبط نگاشت کنید، اما جزئیات داخلی Database، Stack Trace یا اطلاعات حساس را به کاربر نمایش ندهید.
Reset کردن فرم
برای بازگرداندن فرم به مقدار اولیه:
const {
reset
} = useForm<FormValues>({
defaultValues
})
پس از Submit موفق:
async function onSubmit(data: FormValues) {
await saveData(data)
reset()
}
Reset با مقدار جدید:
reset({
name: 'امیر',
email: 'user@example.com'
})
گاهی لازم است برخی وضعیتها حفظ شوند:
reset(undefined, {
keepErrors: false,
keepDirtyValues: true
})
استفاده از این گزینهها باید آگاهانه باشد؛ زیرا نگه داشتن Error یا Dirty State پس از بارگذاری داده جدید میتواند کاربر را سردرگم کند.
setValue و getValues
برای تغییر برنامهنویسیشده یک فیلد:
setValue('temperature', 0.2, {
shouldDirty: true,
shouldValidate: true
})
دریافت مقادیر فعلی بدون Subscription:
const values = getValues()
const prompt = getValues('prompt')
getValues باعث Re-render در اثر تغییر مقدار نمیشود و فقط مقدار لحظهای را برمیگرداند.
watch و useWatch
برای مشاهده تغییر یک یا چند فیلد میتوان از watch استفاده کرد:
const prompt = watch('prompt')
مثال شمارش کاراکتر:
<p>
{prompt?.length ?? 0} از ۴۰۰۰ کاراکتر
</p>
مشاهده چند مقدار:
const [
model,
temperature
] = watch([
'model',
'temperature'
])
اگر فقط یک کامپوننت داخلی باید تغییرات را دنبال کند، useWatch میتواند Re-render را محدودتر کند:
import {
useWatch,
type Control
} from 'react-hook-form'
type PromptCounterProps = {
control: Control<AssistantFormInput>
}
export function PromptCounter({
control
}: PromptCounterProps) {
const prompt = useWatch({
control,
name: 'prompt'
})
return (
<p aria-live="polite">
{(prompt?.length ?? 0).toLocaleString('fa-IR')}
{' از '}
۴٬۰۰۰ کاراکتر
</p>
)
}
از watch() بدون نام در فرمهای بزرگ با احتیاط استفاده کنید؛ زیرا تمام فرم را Subscribe میکند و میتواند Re-renderهای بیشتری ایجاد کند.
trigger برای اجرای دستی Validation
اعتبارسنجی یک فیلد:
const isValidPrompt = await trigger('prompt')
اعتبارسنجی چند فیلد:
const canContinue = await trigger([
'prompt',
'model'
])
اعتبارسنجی تمام فرم:
const isFormValid = await trigger()
این قابلیت برای فرمهای چندمرحلهای مفید است:
async function goToNextStep() {
const isStepValid = await trigger([
'name',
'email'
])
if (isStepValid) {
setStep(2)
}
}
Controller چیست؟
register برای Inputهای استاندارد HTML مناسب است. برخی کتابخانههای UI کامپوننتهای Controlled ارائه میدهند و بهجای ref و name استاندارد، از Propsهایی مانند value و onValueChange استفاده میکنند.
در چنین شرایطی از Controller استفاده میشود:
import {
Controller,
useForm
} from 'react-hook-form'
type FormValues = {
temperature: number
}
export function SettingsForm() {
const {
control,
handleSubmit
} = useForm<FormValues>({
defaultValues: {
temperature: 0.3
}
})
return (
<form
onSubmit={handleSubmit(console.log)}
>
<Controller
name="temperature"
control={control}
render={({ field, fieldState }) => (
<div>
<label>
Temperature: {field.value}
</label>
<input
type="range"
min="0"
max="2"
step="0.1"
value={field.value}
onChange={(event) =>
field.onChange(
Number(event.target.value)
)
}
onBlur={field.onBlur}
ref={field.ref}
/>
{fieldState.error ? (
<p role="alert">
{fieldState.error.message}
</p>
) : null}
</div>
)}
/>
<button type="submit">
ذخیره
</button>
</form>
)
}
در Controller باید ویژگیهای field را به کامپوننت رابط کاربری متصل کنید:
valueonChangeonBlurnameref
برای Input استاندارد، register سادهتر است. استفاده بیدلیل از Controller کد را پیچیدهتر میکند.
useFieldArray برای فیلدهای پویا
فرض کنید کاربر میتواند چند پیام نمونه به فرم اضافه کند:
'use client'
import {
useFieldArray,
useForm
} from 'react-hook-form'
type ExampleFormValues = {
examples: Array<{
input: string
output: string
}>
}
export function DynamicExamplesForm() {
const {
control,
register,
handleSubmit,
formState: {
errors
}
} = useForm<ExampleFormValues>({
defaultValues: {
examples: [
{
input: '',
output: ''
}
]
}
})
const {
fields,
append,
remove
} = useFieldArray({
control,
name: 'examples'
})
return (
<form
onSubmit={handleSubmit(console.log)}
>
{fields.map((field, index) => (
<fieldset key={field.id}>
<legend>
نمونه {index + 1}
</legend>
<label
htmlFor={`examples.${index}.input`}
>
ورودی
</label>
<textarea
id={`examples.${index}.input`}
{...register(
`examples.${index}.input`,
{
required:
'ورودی نمونه الزامی است'
}
)}
/>
{errors.examples?.[index]?.input ? (
<p role="alert">
{
errors.examples[index]
?.input?.message
}
</p>
) : null}
<label
htmlFor={`examples.${index}.output`}
>
خروجی مورد انتظار
</label>
<textarea
id={`examples.${index}.output`}
{...register(
`examples.${index}.output`,
{
required:
'خروجی نمونه الزامی است'
}
)}
/>
<button
type="button"
onClick={() => remove(index)}
disabled={fields.length === 1}
>
حذف نمونه
</button>
</fieldset>
))}
<button
type="button"
onClick={() =>
append({
input: '',
output: ''
})
}
>
افزودن نمونه
</button>
<button type="submit">
ذخیره
</button>
</form>
)
}
برای key از field.id استفاده کنید، نه Index:
<div key={field.id}>
استفاده از Index بهعنوان Key هنگام حذف یا جابهجایی فیلدها میتواند باعث نمایش مقدار اشتباه شود.
ساخت فرم چندمرحلهای
برای فرم چندمرحلهای میتوان تمام دادهها را در یک useForm نگه داشت:
const form = useForm<OnboardingFormValues>({
mode: 'onTouched',
defaultValues: {
name: '',
email: '',
company: '',
useCase: '',
expectedUsage: ''
}
})
حرکت از مرحله اول:
async function goToStepTwo() {
const valid = await trigger([
'name',
'email'
])
if (valid) {
setCurrentStep(2)
}
}
حرکت از مرحله دوم:
async function goToStepThree() {
const valid = await trigger([
'company',
'useCase'
])
if (valid) {
setCurrentStep(3)
}
}
در فرمهای چندمرحلهای:
- داده مرحله قبلی را بیدلیل پاک نکنید.
- Validation هر مرحله را جدا اجرا کنید.
- پیشرفت کاربر را نمایش دهید.
- دکمه بازگشت قرار دهید.
- Submit نهایی را فقط در مرحله آخر انجام دهید.
- اگر فرم طولانی است، ذخیره Draft را در نظر بگیرید.
FormProvider و useFormContext
اگر فرم از چند کامپوننت تو در تو تشکیل شده باشد، ارسال register و errors از طریق چندین لایه Props دشوار میشود.
import {
FormProvider,
useForm
} from 'react-hook-form'
export function ProfileForm() {
const methods =
useForm<ProfileFormValues>({
defaultValues: {
name: '',
email: ''
}
})
return (
<FormProvider {...methods}>
<form
onSubmit={methods.handleSubmit(
console.log
)}
>
<PersonalFields />
<ContactFields />
<button type="submit">
ذخیره
</button>
</form>
</FormProvider>
)
}
در کامپوننت داخلی:
import {
useFormContext
} from 'react-hook-form'
export function PersonalFields() {
const {
register,
formState: {
errors
}
} = useFormContext<ProfileFormValues>()
return (
<div>
<label htmlFor="name">
نام
</label>
<input
id="name"
{...register('name', {
required: 'نام الزامی است'
})}
/>
{errors.name ? (
<p role="alert">
{errors.name.message}
</p>
) : null}
</div>
)
}
از قرار دادن چند FormProvider تو در تو برای یک فرم خودداری کنید.
مدیریت Checkbox
Checkbox تکی:
type FormValues = {
acceptTerms: boolean
}
<input
type="checkbox"
{...register('acceptTerms', {
required:
'پذیرش شرایط استفاده الزامی است'
})}
/>
مجموعه Checkboxها:
type FormValues = {
capabilities: string[]
}
<label>
<input
type="checkbox"
value="text"
{...register('capabilities')}
/>
متن
</label>
<label>
<input
type="checkbox"
value="image"
{...register('capabilities')}
/>
تصویر
</label>
<label>
<input
type="checkbox"
value="audio"
{...register('capabilities')}
/>
صوت
</label>
خروجی:
{
capabilities: ['text', 'image']
}
مدیریت Radio Button
type FormValues = {
responseLength:
| 'short'
| 'medium'
| 'long'
}
<label>
<input
type="radio"
value="short"
{...register('responseLength')}
/>
کوتاه
</label>
<label>
<input
type="radio"
value="medium"
{...register('responseLength')}
/>
متوسط
</label>
<label>
<input
type="radio"
value="long"
{...register('responseLength')}
/>
بلند
</label>
مدیریت Select
<select
{...register('model', {
required: 'یک مدل انتخاب کنید'
})}
>
<option value="">
انتخاب مدل
</option>
<option value="YOUR_MODEL_ID">
مدل انتخابی درواره
</option>
</select>
Model IDهای قابلاستفاده را از صفحه مدلهای درواره دریافت کنید و مقدار واقعی را جایگزین YOUR_MODEL_ID کنید.
مدیریت File Input
type UploadFormValues = {
document: FileList
}
<input
type="file"
accept=".txt,.pdf"
{...register('document', {
required: 'انتخاب فایل الزامی است',
validate: {
fileSize: (files) =>
!files?.[0] ||
files[0].size <= 5 * 1024 * 1024 ||
'حجم فایل نباید بیشتر از ۵ مگابایت باشد',
fileType: (files) =>
!files?.[0] ||
[
'application/pdf',
'text/plain'
].includes(files[0].type) ||
'نوع فایل مجاز نیست'
}
})}
/>
برای ارسال فایل باید از FormData استفاده کنید:
async function onSubmit(
data: UploadFormValues
) {
const file = data.document[0]
const formData = new FormData()
formData.append('document', file)
await fetch('/api/upload', {
method: 'POST',
body: formData
})
}
هنگام ارسال FormData، Header مربوط به Content-Type را دستی تنظیم نکنید؛ مرورگر Boundary صحیح را ایجاد میکند.
Validation نوع و حجم فایل باید در Backend نیز تکرار شود.
غیرفعال کردن فیلد در React Hook Form
فیلد disabled معمولاً در داده Submitشده حضور ندارد:
<input
disabled
{...register('email')}
/>
اگر میخواهید مقدار قابلتغییر نباشد اما همچنان ارسال شود، میتوانید از readOnly استفاده کنید:
<input
readOnly
{...register('email')}
/>
بین disabled و readOnly تفاوت وجود دارد و انتخاب آنها باید براساس رفتار موردنظر انجام شود.
فوکوس روی اولین خطا
React Hook Form میتواند هنگام Submit روی اولین فیلد نامعتبر Focus کند:
useForm<FormValues>({
shouldFocusError: true
})
همچنین میتوان دستی Focus کرد:
setFocus('email')
برای کارکرد صحیح Focus، Ref فیلد باید به React Hook Form متصل باشد.
فرم کامل Prompt برای API هوش مصنوعی
اکنون یک مثال عملی میسازیم که:
- Prompt را دریافت میکند.
- Model ID را میگیرد.
- Temperature را کنترل میکند.
- ورودی را با Zod اعتبارسنجی میکند.
- داده را به Backend میفرستد.
- Loading و Error را نمایش میدهد.
- کلید API را در Client قرار نمیدهد.
// src/components/AiPromptForm.tsx
'use client'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { zodResolver } from '@hookform/resolvers/zod'
const promptFormSchema = z.object({
prompt: z
.string()
.trim()
.min(10, 'حداقل ۱۰ کاراکتر وارد کنید')
.max(
4000,
'متن نمیتواند بیشتر از ۴۰۰۰ کاراکتر باشد'
),
model: z
.string()
.trim()
.min(1, 'Model ID الزامی است'),
temperature: z
.number()
.min(0)
.max(2)
})
type PromptFormInput = z.input<
typeof promptFormSchema
>
type PromptFormOutput = z.output<
typeof promptFormSchema
>
type ApiResponse = {
answer?: string
error?: string
}
export function AiPromptForm() {
const [answer, setAnswer] = useState('')
const {
register,
handleSubmit,
setError,
watch,
reset,
formState: {
errors,
isSubmitting
}
} = useForm<
PromptFormInput,
unknown,
PromptFormOutput
>({
resolver: zodResolver(promptFormSchema),
mode: 'onTouched',
defaultValues: {
prompt: '',
model: 'YOUR_MODEL_ID',
temperature: 0.3
}
})
const prompt = watch('prompt')
async function onSubmit(
data: PromptFormOutput
) {
setAnswer('')
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
const result =
(await response.json()) as ApiResponse
if (!response.ok) {
setError('root.server', {
type: 'server',
message:
result.error ||
'دریافت پاسخ ناموفق بود'
})
return
}
if (!result.answer) {
setError('root.server', {
type: 'server',
message:
'پاسخ معتبری دریافت نشد'
})
return
}
setAnswer(result.answer)
} catch {
setError('root.server', {
type: 'network',
message:
'ارتباط با سرور برقرار نشد'
})
}
}
return (
<section>
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="prompt">
پرامپت
</label>
<textarea
id="prompt"
rows={8}
maxLength={4000}
placeholder="درخواست خود را دقیق بنویسید..."
{...register('prompt')}
aria-invalid={Boolean(errors.prompt)}
/>
<p aria-live="polite">
{(prompt?.length ?? 0)
.toLocaleString('fa-IR')}
{' از '}
۴٬۰۰۰ کاراکتر
</p>
{errors.prompt ? (
<p role="alert">
{errors.prompt.message}
</p>
) : null}
</div>
<div>
<label htmlFor="model">
Model ID
</label>
<input
id="model"
{...register('model')}
aria-invalid={Boolean(errors.model)}
/>
{errors.model ? (
<p role="alert">
{errors.model.message}
</p>
) : null}
</div>
<div>
<label htmlFor="temperature">
Temperature
</label>
<input
id="temperature"
type="number"
min="0"
max="2"
step="0.1"
{...register('temperature', {
valueAsNumber: true
})}
aria-invalid={
Boolean(errors.temperature)
}
/>
{errors.temperature ? (
<p role="alert">
{errors.temperature.message}
</p>
) : null}
</div>
{errors.root?.server ? (
<p role="alert">
{errors.root.server.message}
</p>
) : null}
<div>
<button
type="submit"
disabled={isSubmitting}
>
{isSubmitting
? 'در حال پردازش...'
: 'ارسال درخواست'}
</button>
<button
type="button"
onClick={() => {
reset()
setAnswer('')
}}
disabled={isSubmitting}
>
پاک کردن
</button>
</div>
</form>
{answer ? (
<article aria-live="polite">
<h2>پاسخ مدل</h2>
<p>{answer}</p>
</article>
) : null}
</section>
)
}
Route Handler امن در Next.js
کلید API باید در فایل .env.local قرار گیرد:
DARVAREH_API_KEY=YOUR_DARVAREH_API_KEY
Route Handler:
// src/app/api/chat/route.ts
import { z } from 'zod'
const requestSchema = z.object({
prompt: z
.string()
.trim()
.min(10)
.max(4000),
model: z
.string()
.trim()
.min(1),
temperature: z
.number()
.min(0)
.max(2)
})
type DarvarehResponse = {
choices?: Array<{
message?: {
content?: string
}
}>
}
export async function POST(request: Request) {
try {
const body: unknown = await request.json()
const result = requestSchema.safeParse(body)
if (!result.success) {
return Response.json(
{
error: 'اطلاعات فرم معتبر نیست'
},
{
status: 400
}
)
}
const apiKey =
process.env.DARVAREH_API_KEY
if (!apiKey) {
console.error(
'DARVAREH_API_KEY is missing'
)
return Response.json(
{
error: 'تنظیمات سرویس کامل نیست'
},
{
status: 500
}
)
}
const {
prompt,
model,
temperature
} = result.data
const controller = new AbortController()
const timeoutId = setTimeout(() => {
controller.abort()
}, 30_000)
try {
const response = await fetch(
'https://api.darvareh.ir/v1/chat/completions',
{
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model,
temperature,
messages: [
{
role: 'user',
content: prompt
}
]
}),
signal: controller.signal,
cache: 'no-store'
}
)
if (!response.ok) {
console.error(
'Darvareh request failed',
response.status
)
return Response.json(
{
error:
response.status === 429
? 'تعداد درخواستها زیاد است؛ کمی بعد دوباره تلاش کنید'
: 'دریافت پاسخ از سرویس ناموفق بود'
},
{
status:
response.status === 429
? 429
: 502
}
)
}
const data =
(await response.json()) as DarvarehResponse
const answer =
data.choices?.[0]?.message?.content?.trim()
if (!answer) {
return Response.json(
{
error: 'پاسخ معتبری دریافت نشد'
},
{
status: 502
}
)
}
return Response.json({
answer
})
} finally {
clearTimeout(timeoutId)
}
} catch (error) {
if (
error instanceof Error &&
error.name === 'AbortError'
) {
return Response.json(
{
error:
'زمان انتظار درخواست به پایان رسید'
},
{
status: 504
}
)
}
console.error('Chat route error', error)
return Response.json(
{
error:
'خطایی در پردازش درخواست رخ داد'
},
{
status: 500
}
)
}
}
کلید API نباید در Client Component، فایل عمومی، Repository یا متغیر دارای پیشوند NEXT_PUBLIC_ قرار گیرد.
برای دریافت Model ID و بررسی مدلهای قابلاستفاده به صفحه مدلهای درواره مراجعه کنید.
تست فرم React Hook Form
فرم را میتوان با Vitest و React Testing Library آزمایش کرد:
import {
render,
screen
} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import {
describe,
expect,
it,
vi
} from 'vitest'
import {
SimpleForm
} from './SimpleForm'
describe('SimpleForm', () => {
it('برای فیلد خالی خطا نمایش میدهد', async () => {
const user = userEvent.setup()
const onSubmit = vi.fn()
render(
<SimpleForm onSubmit={onSubmit} />
)
await user.click(
screen.getByRole('button', {
name: 'ارسال'
})
)
expect(
await screen.findByRole('alert')
).toHaveTextContent(
'متن درخواست الزامی است'
)
expect(onSubmit).not.toHaveBeenCalled()
})
it('اطلاعات معتبر را ارسال میکند', async () => {
const user = userEvent.setup()
const onSubmit = vi.fn()
render(
<SimpleForm onSubmit={onSubmit} />
)
await user.type(
screen.getByLabelText('متن درخواست'),
'یک عنوان برای مقاله پیشنهاد بده'
)
await user.click(
screen.getByRole('button', {
name: 'ارسال'
})
)
expect(onSubmit).toHaveBeenCalledWith(
{
prompt:
'یک عنوان برای مقاله پیشنهاد بده'
},
expect.anything()
)
})
})
در تست فرم بهتر است رفتار قابلمشاهده کاربر بررسی شود:
- آیا خطا نمایش داده میشود؟
- آیا فیلد نامعتبر مشخص است؟
- آیا Submit برای داده نامعتبر اجرا نمیشود؟
- آیا داده معتبر به تابع Submit میرسد؟
- آیا Loading نمایش داده میشود؟
- آیا خطای Backend قابلفهم است؟
بهینهسازی Performance فرم
React Hook Form برای کاهش Re-render طراحی شده است، اما معماری نامناسب همچنان میتواند عملکرد فرم را کاهش دهد.
راهکارهای پیشنهادی:
- از
watch()بدون نام در کل فرم بزرگ استفاده نکنید. - برای بخشهای جدا از
useWatchاستفاده کنید. - کامپوننتهای سنگین را از فرم جدا کنید.
- Validation پیچیده را بدون نیاز روی هر
onChangeاجرا نکنید. - برای Inputهای استاندارد از
registerاستفاده کنید. Controllerرا فقط برای Controlled Componentها به کار ببرید.- از
field.idبهعنوان Key درuseFieldArrayاستفاده کنید. - فرم بسیار بزرگ را به بخشهای منطقی تقسیم کنید.
- Context فرم را فقط در محدوده لازم قرار دهید.
- از ذخیره همزمان تمام مقادیر فرم در State جداگانه خودداری کنید.
اشتباهات رایج در React Hook Form
استفاده همزمان از register و value
این کد میتواند باعث تداخل مدیریت State شود:
<input
value={name}
{...register('name')}
/>
یا فیلد را با register مدیریت کنید یا اگر Controlled است از Controller استفاده کنید.
فراموش کردن defaultValues
نبود مقدار اولیه مشخص میتواند باعث رفتار نامشخص Reset، Dirty State یا Warning مربوط به Controlled و Uncontrolled شود.
اعتماد به Validation سمت Client
Validation Client قابلدور زدن است. Backend باید ورودی را دوباره بررسی کند.
استفاده از disabled برای فیلدی که باید ارسال شود
فیلد Disabled معمولاً در خروجی فرم قرار نمیگیرد. در صورت نیاز از readOnly یا مقدار جایگزین استفاده کنید.
استفاده از Index بهعنوان Key
در useFieldArray از field.id استفاده کنید:
key={field.id}
قراردادن کلید API در فرم
React Hook Form محل نگهداری Secret نیست. کلید API باید در Backend یا Route Handler باقی بماند.
اعتبارسنجی Async در هر کاراکتر
بررسیهایی مانند موجود بودن Username در سرور را نباید بدون Debounce با هر تغییر اجرا کنید. این کار تعداد درخواستها را افزایش میدهد.
ثبت اطلاعات حساس در Console
در محیط Production دادههایی مانند رمز عبور، Token، اطلاعات هویتی یا محتوای محرمانه فرم را در Console ثبت نکنید.
استفاده افراطی از Controller
برای Input، Select و Textarea استاندارد معمولاً register کافی است.
ارسال چندباره فرم
دکمه Submit را هنگام isSubmitting غیرفعال کنید و در Backend نیز از راهکار مناسب برای جلوگیری از عملیات تکراری استفاده کنید.
چکلیست ساخت فرم حرفهای
- آیا Type تمام فیلدها تعریف شده است؟
- آیا
defaultValuesمشخصاند؟ - آیا Label هر فیلد به Input متصل است؟
- آیا پیام خطا واضح و نزدیک فیلد نمایش داده میشود؟
- آیا
aria-invalidوrole="alert"استفاده شدهاند؟ - آیا Validation سمت Client و Server وجود دارد؟
- آیا Submit Async خطا را مدیریت میکند؟
- آیا دکمه هنگام ارسال غیرفعال میشود؟
- آیا کاربر Loading State را میبیند؟
- آیا خطاهای Backend به فیلد درست نگاشت میشوند؟
- آیا اطلاعات حساس در Client ذخیره نشدهاند؟
- آیا کلید API فقط در Backend قرار دارد؟
- آیا File Type و File Size در سرور نیز بررسی میشوند؟
- آیا Reset فرم رفتار صحیحی دارد؟
- آیا فیلدهای پویا Key پایدار دارند؟
- آیا فرم با Keyboard قابلاستفاده است؟
- آیا تست سناریوهای موفق و ناموفق نوشته شده است؟
- آیا متن دکمهها و خطاها برای کاربر قابلفهم است؟
پرسشهای متداول درباره React Hook Form
آیا React Hook Form فقط برای TypeScript است؟
خیر. این کتابخانه در JavaScript و TypeScript قابلاستفاده است؛ اما TypeScript ایمنی و تجربه توسعه بهتری فراهم میکند.
React Hook Form بهتر است یا Formik؟
هر دو برای مدیریت فرم ساخته شدهاند. React Hook Form معمولاً بر استفاده از Inputهای Uncontrolled، کاهش Re-render و API مبتنی بر Hook تمرکز دارد. انتخاب نهایی به معماری پروژه و تجربه تیم بستگی دارد.
آیا استفاده از Zod اجباری است؟
خیر. میتوانید از قوانین داخلی register، تابع validate یا Resolverهای دیگر استفاده کنید. Zod برای تعریف Schema مشترک و Type-safe مفید است.
آیا میتوان یک Schema را در Client و Server استفاده کرد؟
بله، اگر پروژه Full-Stack مانند Next.js دارید میتوانید Schema مشترک را در هر دو سمت استفاده کنید. با این حال باید مطمئن شوید فایل مشترک به وابستگیهای مخصوص سرور یا Client وابسته نیست.
تفاوت register و Controller چیست؟
register برای Inputهای استاندارد HTML مناسب است. Controller برای کامپوننتهای Controlled و کتابخانههای UI که API متفاوتی دارند استفاده میشود.
تفاوت watch و getValues چیست؟
watch تغییر مقدار را دنبال کرده و باعث بروزرسانی بخش Subscribeشده میشود. getValues فقط مقدار فعلی را بدون Subscription دریافت میکند.
آیا میتوان React Hook Form را در Next.js استفاده کرد؟
بله. کامپوننت فرم باید بهدلیل استفاده از Hook و Event Handler یک Client Component باشد. پردازش امن، Secret و ارتباط با API خارجی باید در Route Handler یا Backend انجام شود.
آیا میتوان با React Hook Form فرم چتبات ساخت؟
بله. Prompt، تنظیمات مدل، فایل و سایر ورودیها را میتوان با React Hook Form مدیریت کرد و داده معتبر را به Backend فرستاد.
آیا کلید درواره را باید داخل فرم قرار دهیم؟
خیر. کلید API باید در Environment Variable سمت سرور نگهداری شود. Client فقط باید Endpoint داخلی برنامه را فراخوانی کند.
جمعبندی
React Hook Form ابزار مناسبی برای مدیریت فرمهای React و Next.js است. این کتابخانه با APIهایی مانند useForm، register، Controller، useFieldArray، FormProvider و setError بسیاری از نیازهای رایج فرم را پوشش میدهد.
مسیر پیشنهادی یادگیری آن چنین است:
- ابتدا
useForm،registerوhandleSubmitرا یاد بگیرید. - خطاها را با
formState.errorsنمایش دهید. - مقدار اولیه را با
defaultValuesمشخص کنید. - Submitهای Async را با
isSubmittingمدیریت کنید. - برای فرمهای پیچیده از Zod استفاده کنید.
- Validation را در Backend نیز تکرار کنید.
- برای کامپوننتهای Controlled از
Controllerاستفاده کنید. - فیلدهای پویا را با
useFieldArrayبسازید. - خطاهای Server را با
setErrorنمایش دهید. - رفتار فرم را با Testing Library آزمایش کنید.
برای ساخت برنامههای مبتنی بر هوش مصنوعی میتوانید فرم React را به Backend متصل کرده و از API یکپارچه درواره استفاده کنید. فهرست مدلها، قابلیتها و Model IDهای قابلاستفاده در صفحه مدلهای درواره قرار دارد.
مقالات مرتبط
- ساخت چتبات هوش مصنوعی با Next.js، React و API درواره
- ساخت اپلیکیشن با هوش مصنوعی؛ راهنمای کامل و عملی
- برنامهنویسی با ChatGPT؛ راهنمای کامل برای توسعهدهندگان
- ساخت وبسایت با هوش مصنوعی؛ راهنمای کامل
- تولید Unit Test با هوش مصنوعی؛ راهنمای تست نرمافزار
- ساخت داده آزمایشی و Mock API با هوش مصنوعی
- دیباگ کد و رفع خطا با هوش مصنوعی
منابع تکمیلی
- مستندات رسمی React Hook Form
- راهنمای شروع React Hook Form
- مستندات useForm
- مستندات Controller
- مستندات useFieldArray
- Resolverهای رسمی React Hook Form
- مستندات Zod
برای مطالعه شرایط استفاده و محدودیتهای مسئولیت، صفحه «سلب مسئولیت» را مشاهده کنید.