• صفحه اصلی
  • درباره من
  • مهارت‌های ایجنت
  • پروژه‌ها
  • بلاگ
  • مشاوره
  • تماس با من
مشاورهرزومه
Naser Rasouli

نویسنده

Naser Rasouli

توسعه‌دهنده فرانت‌اند؛ اینجا تجربه‌ها و یادداشت‌های واقعی‌ام از پروژه‌ها را می‌نویسم.

GitHubLinkedIn

آخرین نوشته‌ها

Optimistic UI در React بدون باگ‌های رایج
2026-09-11•1 دقیقه مطالعه

Optimistic UI در React بدون باگ‌های رایج

یک الگوی عملی برای Optimistic UI در React با useOptimistic، Transition، rollback و همگام‌سازی نتیجه واقعی سرور بدون ساخت state موازی و پیچیده.

دیباگ INP در اپ‌های React از Field Data تا Profiler
2026-09-10•1 دقیقه مطالعه

دیباگ INP در اپ‌های React از Field Data تا Profiler

یک روند عملی برای پیدا کردن interactionهای کند در React؛ از Field Data و Long Task تا React Profiler، event handlerهای سنگین و کاهش کار روی main thread.

JavaScript SEO برای React: Google دقیقاً چه چیزی می‌بیند؟
2026-09-10•1 دقیقه مطالعه

JavaScript SEO برای React: Google دقیقاً چه چیزی می‌بیند؟

راهنمای عملی JavaScript SEO در React؛ تفاوت crawl و render و index، انتخاب SSR یا prerender و جلوگیری از پنهان‌شدن محتوا و لینک‌ها پشت client rendering.

فرم‌های Type-safe در React با React Hook Form و Zod

فرم‌های Type-safe در React با React Hook Form و Zod

2026-09-07
reacttypescriptreact-hook-formzodforms

فرم‌های Type-safe در React با React Hook Form و Zod

فرم‌ها معمولاً ساده شروع می‌شوند: چند input، یک submit و چند شرط ساده. اما با رشد پروژه، validationهای پراکنده، typeهای تکراری، خطاهای API، fieldهای شرطی و آرایه‌های داینامیک به‌سرعت فرم را سخت‌نگهداری می‌کنند.

الگویی که برای پروژه‌های React قابل‌اعتمادتر است این است که schema منبع اصلی حقیقت باشد، React Hook Form مدیریت state و interaction را انجام دهد و Zod مسئول runtime validation و مدل داده باشد.

چرا schema-first؟

اگر type و validation جدا تعریف شوند، احتمال drift بالا می‌رود:

type ProfileForm = {
  name: string
  age: number
}

بعد ممکن است validation جای دیگری تغییر کند و type همان قبلی بماند. با Zod می‌توان schema و type را به هم متصل کرد:

import { z } from "zod"

const profileSchema = z.object({
  name: z.string().trim().min(2),
  age: z.coerce.number().int().min(18),
})

type ProfileForm = z.infer<typeof profileSchema>

Zod علاوه بر z.infer، وقتی transform یا coercion باعث تفاوت ورودی و خروجی می‌شود، z.input و z.output هم دارد.

اتصال به React Hook Form

Resolver اجازه می‌دهد Zod مستقیماً در چرخه React Hook Form استفاده شود:

import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"

const form = useForm<ProfileForm>({
  resolver: zodResolver(profileSchema),
  defaultValues: {
    name: "",
    age: 18,
  },
})

در فرم‌های ساده این الگو کافی است. اگر schema داده را transform می‌کند، بهتر است تفاوت input و output را آگاهانه مدیریت کنیم.

خطاها را نزدیک همان فیلد نگه دارید

<input
  {...form.register("name")}
  aria-invalid={Boolean(form.formState.errors.name)}
/>

{form.formState.errors.name?.message && (
  <p role="alert">{form.formState.errors.name.message}</p>
)}

خطای مربوط به یک فیلد بهتر است نزدیک همان فیلد نمایش داده شود. این کار هم UX را بهتر می‌کند و هم برای accessibility قابل‌فهم‌تر است.

validation سمت کلاینت مرز امنیت نیست

قاعده مهم:

Client validation = UX
Server validation = Trust boundary

حتی اگر Zod در فرانت‌اند تمام ورودی‌ها را validate کند، کاربر می‌تواند request را خارج از UI ارسال کند. backend باید داده را مستقل بررسی کند.

خطاهای API را به فرم برگردانید

بعضی خطاها در schema قابل تشخیص نیستند؛ مثلاً ایمیلی که قبلاً ثبت شده است:

form.setError("email", {
  type: "server",
  message: "این ایمیل قبلاً ثبت شده است",
})

خطاهای field-level را با setError به همان فیلد متصل کنید و خطاهای عمومی را در سطح فرم نگه دارید.

Field Array

برای لیست‌های داینامیک مثل شماره تماس:

const schema = z.object({
  phones: z.array(
    z.object({
      label: z.string().min(1),
      value: z.string().min(7),
    })
  ).min(1),
})

و در React Hook Form:

const phones = useFieldArray({
  control: form.control,
  name: "phones",
})

یک اشتباه رایج این است که همان آرایه هم در React Hook Form و هم در useState نگهداری شود. دو منبع state معمولاً synchronization را سخت‌تر می‌کنند.

فیلدهای شرطی را در schema مدل کنید

برای فرم‌هایی که shape آن‌ها بر اساس نوع حساب تغییر می‌کند، discriminated union خواناتر است:

const schema = z.discriminatedUnion("accountType", [
  z.object({
    accountType: z.literal("personal"),
    fullName: z.string().min(2),
  }),
  z.object({
    accountType: z.literal("company"),
    companyName: z.string().min(2),
    taxId: z.string().min(5),
  }),
])

async validation را محدود کنید

بررسی uniqueness یا invitation code ممکن است به شبکه نیاز داشته باشد. بهتر است چنین validationهایی را روی هر keystroke اجرا نکنیم.

الگوی مناسب‌تر:

  1. validation ساختاری در کلاینت؛
  2. async check هدفمند در صورت نیاز UX؛
  3. validation نهایی سمت سرور هنگام submit.

اگر schema refinement یا transform async دارد، باید از parse async استفاده شود.

ساختار پیشنهادی

features/profile-form/
├── profile.schema.ts
├── profile-form.tsx
├── profile-form.api.ts
└── profile-form.test.tsx

هدف جداسازی مسئولیت‌هاست، نه زیاد کردن بی‌دلیل فایل‌ها.

چه چیزهایی را تست کنیم؟

به‌جای implementation detail، رفتار فرم را تست کنید:

  • داده نامعتبر submit نشود؛
  • پیام خطا نمایش داده شود؛
  • داده معتبر به handler برسد؛
  • خطای API روی فیلد درست map شود؛
  • field array درست اضافه و حذف شود.

جمع‌بندی

ترکیب React Hook Form و Zod زمانی ارزش واقعی دارد که یک قرارداد معماری بسازد:

Zod Schema
   ↓
Types + Runtime Validation
   ↓
React Hook Form
   ↓
User Interaction
   ↓
API
   ↓
Server Validation

وقتی schema منبع حقیقت باشد و مرز validation کلاینت/سرور روشن بماند، فرم‌های بزرگ قابل‌پیش‌بینی‌تر و refactor آن‌ها ساده‌تر می‌شود.

منابع

  • https://zod.dev/basics
  • https://zod.dev/api
  • https://github.com/react-hook-form/resolvers