React Hook Form — Zod bilan schema validation

~20 daq · React · Formalar

Muammodan boshlaylik

64-darsdagi forma qoidalari JSX bo'ylab tarqalgan:

<input {...register('email', { required: '...', pattern: { value: /.../, message: '...' } })} />
<input {...register('yosh', { valueAsNumber: true, min: { ... }, max: { ... } })} />
<input {...register('parolTakror', { validate: (v) => v === getValues('parol') || '...' })} />

Muammolar:

  • Qoidalarni qayta ishlatib bo'lmaydi — ro'yxatdan o'tish va profil tahrirlashda email qoidasi takrorlanadi
  • Backend da (Node.js) xuddi shu qoidalarni qaytadan yozish kerak
  • TypeScript da forma turi (type Forma = { email: string; yosh: number }) alohida yoziladi va qoidalar bilan farq qilib qolishi mumkin
  • Qoidalarni komponentsiz testlab bo'lmaydi

Yechim — barcha qoidalarni bitta joyda, sxema sifatida yozish. Eng mashhur sxema kutubxonasi — Zod.


Zod nima

npm install zod @hookform/resolvers
  • zod — sxema kutubxonasi
  • @hookform/resolvers — React Hook Form ni Zod (va boshqa kutubxonalar) bilan bog'lovchi

Zod da ma'lumot qanday bo'lishi kerakligini tasvirlaysiz:

import { z } from 'zod';

const foydalanuvchiSchema = z.object({
  ism: z.string().min(2),
  yosh: z.number().int().min(18),
  email: z.string().email(),
});

foydalanuvchiSchema.parse({ ism: 'Ali', yosh: 25, email: 'ali@mail.uz' });   // ✅ ma'lumotni qaytaradi
foydalanuvchiSchema.parse({ ism: 'A', yosh: 15, email: 'ali' });             // ❌ ZodError — 3 ta xato

const natija = foydalanuvchiSchema.safeParse(malumot);
if (!natija.success) console.log(natija.error.issues);                       // throw siz

Zod React ga bog'liq emas — u istalgan JavaScript kodida ishlaydi: frontend, backend, API javoblarini tekshirish.

[!INFO] Zod ning 4-versiyasida email kabi format tekshiruvlari uchun yuqori darajadagi funksiyalar qo'shildi: z.email(), z.url(). Eski z.string().email() sintaksisi ham ishlayveradi. Bu darsdagi misollar ikkala versiyada ishlaydi.


Asosiy turlar va qoidalar

z.string()                       // matn
  .min(2, 'Kamida 2 belgi')
  .max(50, "Ko'pi bilan 50 belgi")
  .email("Email noto'g'ri")
  .url("URL noto'g'ri")
  .regex(/^\+998\d{9}$/, 'Format: +998XXXXXXXXX')
  .trim()                        // bo'sh joylarni olib tashlash

z.number()                       // son
  .int('Butun son bo\'lsin')
  .positive()
  .min(18, '18 yoshdan katta')
  .max(100)

z.boolean()                      // true / false
z.date()                         // Date
z.enum(['kuryer', 'pochta', 'ozi'])        // faqat shu qiymatlardan biri
z.array(z.string()).min(1, 'Kamida bittasini tanlang')
z.object({ ... })

.optional()                      // undefined bo'lishi mumkin
.nullable()                      // null bo'lishi mumkin
.default('qiymat')               // bo'lmasa — standart

Har bir qoidaning oxirgi argumenti — xato xabari.


React Hook Form bilan ulash

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const royxatSchema = z.object({
  ism: z.string().trim().min(2, 'Ism kamida 2 harf'),
  email: z.string().email("Email formati noto'g'ri"),
  yosh: z.coerce.number().int().min(16, '16 yoshdan katta bo\'lishi kerak'),
  shartlar: z.boolean().refine((v) => v === true, { message: 'Shartlarga rozilik bering' }),
});

function Royxat() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm({
    resolver: zodResolver(royxatSchema),
    defaultValues: { ism: '', email: '', yosh: '', shartlar: false },
    mode: 'onTouched',
  });

  async function onSubmit(data) {
    // data — Zod tomonidan tekshirilgan va o'zgartirilgan: yosh — son, ism — trim qilingan
    await api.royxat(data);
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)} noValidate>
      <input {...register('ism')} />
      {errors.ism && <p role="alert">{errors.ism.message}</p>}

      <input type="email" {...register('email')} />
      {errors.email && <p role="alert">{errors.email.message}</p>}

      <input type="number" {...register('yosh')} />
      {errors.yosh && <p role="alert">{errors.yosh.message}</p>}

      <label>
        <input type="checkbox" {...register('shartlar')} /> Shartlarga roziman
      </label>
      {errors.shartlar && <p role="alert">{errors.shartlar.message}</p>}

      <button disabled={isSubmitting}>Ro'yxatdan o'tish</button>
    </form>
  );
}
  • register('ism') — endi qoidasiz; hamma qoida sxemada
  • resolver: zodResolver(schema) — RHF validatsiyani Zod ga topshiradi
  • onSubmit ga keladigan data — tekshirilgan va o'zgartirilgan ma'lumot

z.coerce — tur o'zgartirish

z.coerce.number()     // "25" → 25
z.coerce.date()       // "2026-11-07" → Date

Input lar doim string beradi (62-dars). z.coerce uni kerakli turga aylantiradi — valueAsNumber kerak emas.

[!WARNING] z.coerce.number() bo'sh string "" ni 0 ga aylantiradi — "majburiy maydon" tekshiruvi ishlamay qoladi. Majburiy son maydoni uchun z.coerce.number().min(1, ...) yoki z.string().min(1, 'Kiriting').pipe(z.coerce.number()) kabi naqshdan foydalaning.


Bog'liq maydonlar — refine

Parollar mosligi bir maydonga emas, ikki maydonga bog'liq — refine obyekt darajasida:

const parolSchema = z
  .object({
    parol: z
      .string()
      .min(8, 'Kamida 8 belgi')
      .regex(/\d/, 'Kamida bitta raqam')
      .regex(/[A-Z]/, 'Kamida bitta katta harf'),
    parolTakror: z.string(),
  })
  .refine((d) => d.parol === d.parolTakror, {
    message: 'Parollar mos kelmadi',
    path: ['parolTakror'],                 // xato qaysi maydonda ko'rinadi
  });

path siz xato errors.root ga tushadi — foydalanuvchi uni maydon yonida ko'rmaydi.

Shartli qoidalar

const buyurtmaSchema = z
  .object({
    yetkazish: z.enum(['kuryer', 'ozi']),
    manzil: z.string().optional(),
  })
  .refine((d) => d.yetkazish !== 'kuryer' || (d.manzil && d.manzil.length > 5), {
    message: 'Kuryer uchun manzilni kiriting',
    path: ['manzil'],
  });

Kuryer tanlanganda manzil majburiy, aks holda — yo'q.


TypeScript — tur avtomatik

const royxatSchema = z.object({
  ism: z.string().min(2),
  email: z.string().email(),
  yosh: z.coerce.number(),
});

type RoyxatForma = z.infer<typeof royxatSchema>;
// { ism: string; email: string; yosh: number }

const { register } = useForm<RoyxatForma>({
  resolver: zodResolver(royxatSchema),
});

register('isim');   // ❌ TypeScript xatosi — bunday maydon yo'q

z.infer — sxemadan tur chiqaradi. Tur va qoidalar hech qachon farq qilib qolmaydi: bitta manba.


Sxemani qayta ishlatish

// schemas/foydalanuvchi.js
export const emailSchema = z.string().trim().toLowerCase().email("Email noto'g'ri");
export const parolSchema = z.string().min(8, 'Kamida 8 belgi');

export const loginSchema = z.object({
  email: emailSchema,
  parol: z.string().min(1, 'Parolni kiriting'),
});

export const royxatSchema = z.object({
  ism: z.string().min(2),
  email: emailSchema,
  parol: parolSchema,
});

export const profilSchema = royxatSchema.omit({ parol: true }).extend({
  bio: z.string().max(300).optional(),
});
  • Kichik sxemalardan kattalarini yig'ish
  • .omit(), .pick(), .extend(), .partial() — mavjud sxemadan yangisini yasash

Node.js backend da xuddi shu faylni import qilib, API ga kelgan ma'lumotni tekshirish mumkin — frontend va backend qoidalari doim bir xil.


Nima uchun shunday?

Nima uchun validatsiyani komponentdan alohida sxemaga chiqarish yaxshi amaliyot?

Chunki validatsiya qoidalari — bu biznes qoidalari, UI emas. "Foydalanuvchi 16 yoshdan katta bo'lishi kerak" — bu qoida forma qanday ko'rinishidan qat'i nazar mavjud: veb-saytda, mobil ilovada, API da, ma'lumotlar bazasiga yozishda. Uni JSX ichiga yozish — qoidani bitta ko'rinishga "qamab qo'yish" degani. Sxema esa qoidani ma'lumot sifatida ifodalaydi: uni import qilish, birlashtirish, testlash, serverda qayta ishlatish va undan TypeScript turini chiqarish mumkin. Bu dasturlashdagi eski tamoyil — "yagona haqiqat manbai" (single source of truth): har bir bilim tizimda faqat bir joyda yozilsin.


Xulosa

Eslab qoling:

  • Zod — ma'lumot sxemasi: z.object({ ism: z.string().min(2, 'xabar') })
  • npm install zod @hookform/resolvers; useForm({ resolver: zodResolver(schema) })
  • register('nom') — qoidasiz; onSubmit ga tekshirilgan va o'zgartirilgan data keladi
  • z.coerce.number() — string → son; bo'sh string 0 ga aylanishini unutmang
  • .refine(fn, { message, path }) — bog'liq maydonlar va shartli qoidalar
  • z.infer<typeof schema> — TypeScript turi avtomatik
  • Sxemalarni bo'lish va qayta ishlatish: .omit, .pick, .extend, .partial; backend da ham

Keyingi dars

66-dars: React Hook Form — murakkab formalar, massiv maydonlar