IlmHamroh
JavaScript Full-stack/19-qism. React: routing, server state, formalar31/36-dars23 daqiqa
Mundarija (30)

React Hook Form va Zod: zodResolver, umumiy sxema va server xatolari

Qisqacha: useForm({ resolver: zodResolver(schema) }) — formaning hamma qoidalari endi bitta Zod sxemasida, register da qoida yozilmaydi. Resolver sxema xatolarini errors.<maydon>.message ga joylaydi, handleSubmit esa sxemaning chiqish qiymatini (kesilgan matn, son) beradi. defaultValues — sxemaning kirish turida. Server rad etsa — setError("maydon", …) yoki butun formaga setError("root.server", …). Sxema alohida faylda tursa, uni server ham ishlatadi.

Bu darsda

  • @hookform/resolvers/zod bilan formani sxemaga ulaysiz va resolver ichida nima bo'lishini o'z resolveringizni yozib ko'rasiz.
  • Formada z.input (maydonlar) va z.output (yuborish) farqini ishlatasiz, defaultValues ni to'g'ri berasiz.
  • Ma'lumotga bog'liq sxemani (takror tekshiruvi) "sxema fabrikasi" va useMemo bilan yasaysiz.
  • Server xatolarini maydonga va butun formaga setError bilan chiqarasiz.
  • Sxemani brauzer va server o'rtasida ulashish tuzilmasini bilasiz.

Oldin bilishingiz kerak: React Hook Form I, Zod chuqurroq, TanStack Query III: mutatsiyalar, memo, useMemo, useCallback.

1. Nega bu kerak?

Ikki darsda ikki narsa qurdik. RHF I da qoidalar register ichida edi: required, min, pattern. Zod chuqurroq da xuddi shu qoidalar sxemada: z.coerce.number().min(1), .refine(…, { path }). Endi Sardorda ikki nusxa bor. Jasur aka "12 kishi emas, 15 kishigacha" desa, Sardor ikki joyni o'zgartirishi kerak — va bittasini unutadi.

Uchinchi nusxa ham kelyapti: server. Serverchi ham xuddi shu qoidalarni yozadi. Uch joyda bitta qoida — uch xil xato.

Yechim — qoidalar faqat sxemada, forma esa sxemadan foydalanadi. RHF ning resolver imkoniyati aynan shu uchun. Resolver (hal qiluvchi) — RHF bilan sxema kutubxonasi o'rtasidagi "tarjimon". Elektr vilkasi va rozetka o'rtasidagi adapterga o'xshaydi: RHF bitta shakldagi vilka, Zod, Valibot, Yup — har xil rozetkalar. Adapter almashadi, forma o'zgarmaydi.

2. zodResolver — formani sxemaga ulash

2.1 Birinchi ulanish

Paket — @hookform/resolvers (5.9.1), Zod uchun yo'li @hookform/resolvers/zod (RHF I darsida o'rnatilgan). Bron formasining qoidalari endi sxemada, register toza:

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

const emptyToUndefined = (v) => (v === "" ? undefined : v);

const Booking = z.object({
  name: z.string().trim().min(2, "Ism kamida 2 harf"),
  guests: z.preprocess(
    emptyToUndefined,
    z.coerce
      .number({ error: "Nechta mehmon?" })
      .int("Butun son yozing")
      .min(1, "Kamida 1 kishi")
      .max(12, "Ko'pi bilan 12 kishi"),
  ),
});

export default function App() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    resolver: zodResolver(Booking),
    defaultValues: { name: "", guests: "" },
  });
  return (
    <form noValidate onSubmit={handleSubmit((d) => console.log(d))}>
      <p>
        <label htmlFor="name">Ism</label>
        <input id="name" {...register("name")} />
      </p>
      <p role="alert">{errors.name?.message}</p>
      <p>
        <label htmlFor="guests">Mehmonlar soni</label>
        <input id="guests" type="number" {...register("guests")} />
      </p>
      <p role="alert">{errors.guests?.message}</p>
      <button>Bron qilish</button>
    </form>
  );
}

Konsolda:

text
{name: 'Malika', guests: 4}

Ekranda:

text
Ism
Mehmonlar soni
Bron qilish

Avval bo'sh forma yuborildi — "Ism kamida 2 harf" va "Nechta mehmon?" chiqdi (tekshiruvchi birinchisini oraliq nuqtada ko'rdi). Keyin to'ldirildi va konsolga qarang:

  • name: 'Malika' — mehmon " Malika " yozgan edi. Bo'shliqlar kesildi: handleSubmit sxemaning chiqish qiymatini berdi.
  • guests: 4 — son. valueAsNumber yozmadik, coerce ishladi.

register("guests") qoidasiz. Endi qoidaning yagona joyi — Booking.

2.2 Resolver nima qiladi?

Sehr yo'q. Resolver — oddiy funksiya: forma qiymatlarini oladi va { values, errors } qaytaradi. Agar xato bo'lmasa, values — tozalangan qiymatlar, errors — {}. Xato bo'lsa — teskarisi. O'zimiz yozib ko'ramiz:

jsx
import { useForm } from "react-hook-form";
import { z } from "zod";

const Booking = z.object({
  name: z.string().trim().min(2, "Ism kamida 2 harf"),
});

function myResolver(values) {
  const result = Booking.safeParse(values);
  console.log("resolver:", values);
  if (result.success) {
    return { values: result.data, errors: {} };
  }
  const errors = {};
  for (const issue of result.error.issues) {
    const name = issue.path.join(".");
    errors[name] ??= { type: issue.code, message: issue.message };
  }
  return { values: {}, errors };
}

export default function App() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({ resolver: myResolver, defaultValues: { name: "" } });
  return (
    <form noValidate onSubmit={handleSubmit((d) => console.log(d))}>
      <p>
        <label htmlFor="name">Ism</label>
        <input id="name" {...register("name")} />
      </p>
      <p role="alert">{errors.name?.message}</p>
      <button>Bron qilish</button>
    </form>
  );
}

Konsolda:

text
resolver: {name: 'A'}

Ekranda:

text
Ism
Ism kamida 2 harf
Bron qilish

Yigirma qatorda zodResolver ning asosiy ishi:

  1. qiymatlarni sxemadan o'tkazish (safeParse);
  2. har xatoning path ini maydon nomiga aylantirish (["items", 2, "dish"] → "items.2.dish");
  3. har maydonga birinchi xatoni qo'yish (??= — "bo'sh bo'lsa, yoz", mantiqiy tayinlash).

Haqiqiy zodResolver bundan ko'proq qiladi: ichma-ich obyektlarni to'g'ri yig'adi, async sxemani kutadi (safeParseAsync), RHF ning criteriaMode: "all" sozlamasida bitta maydonning hamma xatolarini yig'adi. Lekin g'oya shu. O'z resolveringizni yozish kamdan-kam kerak bo'ladi — bu misol "qora quti"ni ochish uchun.

sequenceDiagram
  participant F as Forma (RHF)
  participant R as zodResolver
  participant Z as Zod sxema
  F->>R: qiymatlar (satrlar)
  R->>Z: safeParse(qiymatlar)
  Z-->>R: data yoki issues
  R-->>F: { values, errors }
  F->>F: xato yo'q → onSubmit(values)

2.3 Resolver qachon chaqiriladi?

Resolver mode ga bo'ysunadi (RHF I dagi jadval): onSubmit da — yuborishda, keyin reValidateMode bo'yicha. Bir farq bor: resolver butun sxemani tekshiradi, bitta maydonni emas. RHF natijadan faqat kerakli maydon xatosini oladi. 30 maydonli formada mode: "onChange" bo'lsa, har harfda butun sxema ishlaydi. Odatda bu millisekundlar — lekin sxemada og'ir refine (masalan, katta ro'yxatda qidirish) bo'lsa, buni hisobga oling.

Tekshirib ko'ring: Sxemada name: z.string().trim().min(2). Mehmon " Ali " yozdi. Formadagi maydonda nima ko'rinadi va onSubmit nima oladi?

Javob

Maydonda — " Ali " (bo'shliqlar bilan), chunki resolver DOM'dagi qiymatni o'zgartirmaydi. onSubmit esa "Ali" ni oladi — sxemaning chiqishini. Maydon — kirish, onSubmit — chiqish. Mehmon yozganini ko'radi, server tozasini oladi.

3. Kirish va chiqish turlari formada

3.1 defaultValues — kirish turida

Zod chuqurroq darsida ko'rdik: transform yoki coerce bo'lsa, sxemaning ikki turi bor. Formada ular shunday taqsimlanadi:

Joy Tur Bron misolida
defaultValues, maydonlar, watch z.input { name: string; guests: unknown }
handleSubmit(fn) ning fn(data) z.output { name: string; guests: number }

Shuning uchun defaultValues: { guests: "" } — to'g'ri: maydon bo'sh satr bilan boshlanadi. guests: 0 yozsangiz, maydonda 0 turadi — mehmon uni o'chirib yozishi kerak bo'ladi.

TypeScript buni o'zi kuzatadi. Ataylab ikki xato qildik — onSubmit da guests ni satr deb oldik va maydon nomini noto'g'ri yozdik (bu blok — tsc tekshiruvi bilan):

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

const Booking = z.object({
  name: z.string().trim().min(2),
  guests: z.coerce.number().int().min(1),
});

export default function App() {
  const { register, handleSubmit } = useForm({
    resolver: zodResolver(Booking),
    defaultValues: { name: "", guests: "" },
  });
  return (
    <form
      onSubmit={handleSubmit((data) => {
        const total: string = data.guests;
        console.log(total);
      })}
    >
      <input {...register("nmae")} />
    </form>
  );
}

Xato:

text
TS2322: Type 'number' is not assignable to type 'string'.
TS2345: Argument of type '"nmae"' is not assignable to parameter of type '"guests" | "name"'.

Birinchisi: data.guests — number (sxemaning chiqishi), satr emas. Ikkinchisi: register faqat sxemadagi nomlarni qabul qiladi — "nmae" dagi harf xatosi brauzerda emas, yozish paytida ushlandi. defaultValues: { guests: "" } esa xatosiz: coerce ning kirish turi — unknown.

TypeScript'da bu uch tur parametri bilan yoziladi: useForm<Kirish, Kontekst, Chiqish>. zodResolver(schema) ularni sxemadan o'zi chiqaradi, lekin aniq yozish ham mumkin. vazifalar dagi ko'p qadamli forma shunday qiladi (kanon, RHF III darsidan parcha):

tsx
export type LoyihaKirishi = z.input<ReturnType<typeof loyihaSxemasi>>;
export type LoyihaQiymati = z.output<ReturnType<typeof loyihaSxemasi>>;
// ...
  } = useForm<LoyihaKirishi, unknown, LoyihaQiymati>({

Ikkinchi parametr — context (resolverga qo'shimcha ma'lumot), kerak bo'lmasa unknown.

3.2 Nega defaultValues doim kerak

Formaga defaultValues bermaslik ham mumkin. Lekin to'rt sabab bilan biz doim beramiz:

  1. isDirty — "boshlang'ich qiymatdan farq" — boshlang'ichsiz ma'nosiz (RHF I).
  2. Turlar. TypeScript formaning shaklini defaultValues dan ham oladi.
  3. undefined emas, "". Boshlang'ichsiz maydon undefined bilan boshlanadi. Sxemada z.string() bo'lsa, xato matni "expected string, received undefined" bo'ladi — mehmon uchun tushunarsiz.
  4. Qaytarish (reset). Forma tozalanganda nimaga qaytishi — shu qiymatlar (RHF II).

defaultValues funksiya ham bo'lishi mumkin — async () => await api.get(…). Tahrirlash formasida serverdan kelgan ma'lumot bilan to'ldirish uchun. Bu va reset — kelgusi darsda.

4. Ma'lumotga bog'liq sxema

4.1 Sxema fabrikasi

Zod chuqurroq darsida vazifalar sxemasi funksiya edi: tahrirSxemasi(takrormi). Sabab — "bu matn ro'yxatda bormi?" degan qoida ro'yxatni biladi, sxema esa yo'q. Formada uni shunday ulaymiz: ro'yxat o'zgarsa — yangi sxema, o'zgarmasa — o'sha.

«Bahor» menyusiga yangi taom qo'shish formasi. Menyuda bor nom qayta qo'shilmasin:

jsx
import { useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";

function dishSchema(exists) {
  return z.object({
    name: z
      .string()
      .trim()
      .min(2, "Nom kamida 2 harf")
      .refine((n) => !exists(n), "Bu taom menyuda bor"),
  });
}

export default function App() {
  const [menu, setMenu] = useState(["Osh", "Manti"]);
  const schema = useMemo(() => {
    const lower = menu.map((d) => d.toLowerCase());
    return dishSchema((n) => lower.includes(n.toLowerCase()));
  }, [menu]);
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    resolver: zodResolver(schema),
    defaultValues: { name: "" },
  });
  return (
    <form
      noValidate
      onSubmit={handleSubmit((d) => setMenu([...menu, d.name]))}
    >
      <p>Menyu: {menu.join(", ")}</p>
      <p>
        <label htmlFor="name">Taom nomi</label>
        <input id="name" {...register("name")} />
      </p>
      <p role="alert">{errors.name?.message}</p>
      <button>Qo'shish</button>
    </form>
  );
}

Ekranda:

text
Menyu: Osh, Manti
Taom nomi
Bu taom menyuda bor
Qo'shish

"manti" kichik harf bilan yozildi, lekin topildi — taqqoslash toLowerCase bilan. Uch nuqta:

  • dishSchema(exists) — sxema fabrikasi. Uni testda dishSchema(() => true) kabi chaqirib, React'siz sinash mumkin.
  • useMemo — sxema faqat menu o'zgarganda qayta yasaladi (useMemo). Har renderda yangi sxema ham ishlardi, lekin zodResolver(schema) ham har safar yangi funksiya bo'lardi — keraksiz ish.
  • RHF yangi resolver'ni oladi. useForm har renderda berilgan resolver ni ishlatadi, shuning uchun yangi taom qo'shilgach, keyingi tekshiruv yangi menyuni biladi.

Diqqat: Bu tekshiruv faqat brauzerdagi ro'yxatga qaraydi. Ikki xodim bir vaqtda "Lag'mon" ni qo'shsa, ikkalasida ham "yo'q" chiqadi. Haqiqiy kafolat — server (va ma'lumotlar bazasidagi noyoblik sharti). Brauzer tekshiruvi — tez javob, xolos. Server javobi qanday ko'rsatilishi — keyingi bo'limda.

5. Server xatolari: setError

5.1 Maydonga va butun formaga

Sxema hamma narsani bilmaydi. Server javob berdi: "Bu nom band" (409) yoki "Server ishlamayapti" (500). Bu xatolarni ham forma ko'rsatishi kerak. setError(nom, { type, message }) — errors ga qo'lda xato qo'yadi:

  • setError("name", …) — muayyan maydonga: xato maydon ostida chiqadi, fokus berish mumkin.
  • setError("root.server", …) — butun formaga: hech bir maydonga tegishli bo'lmagan xato. root — RHF ning maxsus kaliti, server — o'zingiz tanlagan nom (root.tarmoq, root.ruxsat...).

Soxta server: 200 ms kutadi, "Lag'mon" — band, "Xato" — server yiqildi:

jsx
import { useForm } from "react-hook-form";

function fakeSave(name) {
  return new Promise((resolve) =>
    setTimeout(() => {
      if (name === "Lag'mon") resolve({ status: 409 });
      else if (name === "Xato") resolve({ status: 500 });
      else resolve({ status: 201 });
    }, 200),
  );
}

export default function App() {
  const {
    register,
    handleSubmit,
    setError,
    formState: { errors, isSubmitSuccessful },
  } = useForm({ defaultValues: { name: "" } });

  async function save({ name }) {
    const res = await fakeSave(name);
    if (res.status === 409) {
      setError(
        "name",
        { type: "server", message: "Bu nom serverda band" },
        { shouldFocus: true },
      );
    } else if (res.status !== 201) {
      setError("root.server", {
        message: `Saqlanmadi (server: ${res.status})`,
      });
    }
  }

  return (
    <form noValidate onSubmit={handleSubmit(save)}>
      <p>
        <label htmlFor="name">Taom nomi</label>
        <input id="name" {...register("name")} />
      </p>
      <p role="status">{errors.name?.message}</p>
      <p role="alert">{errors.root?.server?.message}</p>
      <p>Muvaffaqiyat: {String(isSubmitSuccessful)}</p>
      <button>Qo'shish</button>
    </form>
  );
}

Ekranda:

text
Taom nomi
Saqlanmadi (server: 500)
Muvaffaqiyat: false
Qo'shish

Ikki holat ketma-ket. Avval "Lag'mon" — xato maydon ostida ("Bu nom serverda band"), fokus maydonga qaytdi (shouldFocus: true). Keyin "Xato" — maydon xatosi yo'qoldi, forma xatosi chiqdi. isSubmitSuccessful — false: setError chaqirilgan yuborish muvaffaqiyatli hisoblanmaydi.

Nega birinchi xato yo'qoldi? Yangi yuborish boshlanganda RHF eski xatolarni tozalaydi va qaytadan tekshiradi. root xatolari esa boshqacha: ular faqat keyingi yuborishda tozalanadi, mehmon maydonga yozganda emas. Maydon xatosi type: "server" bo'lsa ham, mehmon maydonni o'zgartirganda (reValidateMode) tekshiruv qayta ishlaydi va qoida buzilmagan bo'lsa, xato yo'qoladi.

Xato Qayerga Qachon yo'qoladi
"Bu nom band" (409) setError("name", …) maydon qayta tekshirilganda yoki keyingi yuborishda
"Server ishlamayapti" (5xx) setError("root.server", …) keyingi yuborishda (yoki clearErrors("root"))
"Internet yo'q" root — boshqa nom bilan keyingi yuborishda

5.2 Server xatolari ro'yxat bo'lib kelsa

Ko'p serverlar (ayniqsa Zod bilan yozilganlari) xatolarni maydonlar bo'yicha qaytaradi: { "fieldErrors": { "name": ["Bu nom band"] } } — Zod chuqurroq dagi z.flattenError shakli. Uni formaga bitta sikl bilan o'tkazish mumkin:

js
for (const [field, messages] of Object.entries(body.fieldErrors)) {
  setError(field, { type: "server", message: messages[0] });
}

Server va forma bir sxemani ishlatsa, maydon nomlari ham bir xil — bu sikl ishonchli ishlaydi. Bu keyingi bo'limning asosiy foydasi.

Tekshirib ko'ring: Server xatosini setError("root.server") bilan qo'ydik. Mehmon maydonni to'g'riladi, lekin hali yubormadi. Xato ekranda qoladimi?

Javob

Qoladi. root xatolari maydon o'zgarganda qayta tekshirilmaydi — ular hech bir maydonga bog'lanmagan. Faqat keyingi yuborishda yoki clearErrors("root.server") bilan yo'qoladi. Bu to'g'ri xulq: "server ishlamayapti" xabari mehmon nimadir yozgani bilan haqiqatga aylanmaydi.

6. Umumiy sxemalar: brauzer va server

6.1 Sxema — alohida faylda

Sxema komponent ichida emas, alohida faylda tursin. Ikki sabab: uni React'siz test qilish mumkin (Zod chuqurroq dagi node:test), va uni server ham import qila oladi.

Frontend va backend bitta repoda bo'lsa (monorepo — bir nechta paketli bitta repo, Workspaces va monorepo darsida batafsil), tuzilma odatda shunday:

text
bahor/
├── packages/
│   └── sxemalar/          ← umumiy paket
│       ├── bron.ts        (z.object(...) va turlar)
│       └── package.json   ("name": "@bahor/sxemalar")
├── apps/
│   ├── web/               ← React: zodResolver(bronSxemasi)
│   └── api/               ← server: safeParse(req.body)
flowchart LR
  S["@bahor/sxemalar<br/>bronSxemasi"] --> W["Brauzer<br/>zodResolver"]
  S --> A["Server<br/>safeParse(body)"]
  S --> T["Testlar<br/>node:test"]
  A -- "xato: fieldErrors" --> W

Bitta qoida — uch joy. Jasur aka "15 kishigacha" desa, bitta son o'zgaradi va brauzer ham, server ham, testlar ham yangi qoidani biladi. Server qaytargan fieldErrors dagi nomlar formadagi nomlar bilan bir xil.

6.2 Nimani ulashmaslik kerak

Hamma narsa ham umumiy bo'lmaydi:

  • Faqat serverda bo'ladigan tekshiruvlar — "bu nom bazada bormi?", "foydalanuvchining huquqi bormi?". Ular brauzer sxemasiga kirmaydi (brauzerda baza yo'q). Server ularni umumiy sxemadan keyin tekshiradi va xatoni fieldErrors shaklida qaytaradi — yuqoridagi sikl uni maydonga qo'yadi.
  • Brauzerga xos transform'lar — masalan, mehmon kiritgan "+998 90 …" ni raqamlarga aylantirish. Server odatda allaqachon toza qiymatni oladi, shuning uchun u chiqish turini tekshiradi. Ba'zan buning uchun ikki sxema yoziladi: forma sxemasi (kirish → chiqish) va API sxemasi (faqat chiqish).
  • Xabar matnlari — umumiy bo'lishi mumkin, lekin ko'p tilli saytda ular odatda kalit bo'ladi ("errors.name.min"), matn esa har tomonda tarjimadan (i18n).

Server tomoni — 25-qismda (Zod serverda) va Next.js server action'larida (Server Actions II). vazifalar da hali o'z backendimiz yo'q (soxta «Bahor» API), shuning uchun sxema src/sxemalar/ papkasida turibdi.

7. Vazifalar qadami: sxema formaga ulandi (holat 32)

7.1 zodResolver va defaultValues

Oldingi darsdagi tahrirSxemasi endi VazifaTahriri.tsx ga ulanadi (kanon):

tsx
export function VazifaTahriri() {
  const { vazifa } = useVazifa();
  const { vazifalar } = useVazifalar();
  const yoz = useYozuv();
  const elonQil = useIlovaHolati((h) => h.elonQil);
  const orqaga = useOrqaga(`/vazifa/${vazifa.id}`);
  const xatoId = useId();
  // Sxema ro'yxatga bog'liq (takror matn) — ro'yxat o'zgarsa,
  // yangisi
  const sxema = useMemo(() => {
    const royxat = VazifalarRoyxati.dan(vazifalar);
    return tahrirSxemasi((matn) => royxat.matnBormi(matn, vazifa.id));
  }, [vazifalar, vazifa.id]);
  const {
    register,
    control,
    handleSubmit,
    setError,
    formState: { errors, isDirty, isSubmitting },
  } = useForm({
    // zodResolver (19/#31): yuborishda (va mode bo'yicha)
    // sxema tekshiradi, xatolar — errors.<maydon>.message.
    // Natija turi — sxemaning chiqishi (kesilgan matn)
    resolver: zodResolver(sxema),
    defaultValues: { matn: vazifa.matn, bajarildi: vazifa.bajarildi },
    // Xato — maydondan chiqqanda (blur), keyin — har o'zgarishda
    mode: "onTouched",
  });

Bugungi darsning uch g'oyasi:

  • Sxema fabrikasi + useMemo — «Ma'lumotga bog'liq sxema» bo'limidagi taom misolining o'zi. royxat.matnBormi(matn, vazifa.id) — shu vazifaning o'zidan boshqa vazifada shu matn bormi. Aks holda "o'zgartirmasdan saqlash" ham "takror" bo'lib chiqardi.
  • useVazifalar() — ro'yxat Query keshidan (TanStack Query I). Boshqa tabda yangi vazifa qo'shilsa va kesh yangilansa, sxema ham yangilanadi.
  • defaultValues — vazifaning hozirgi qiymatlari. Ma'lumot tayyor: uni loader chizishdan oldin olgan. Shuning uchun reset yoki async defaultValues kerak emas.

7.2 Saqlash va server xatosi

saqla — handleSubmit ga beriladigan funksiya (kanon):

tsx
  async function saqla(qiymat: TahrirQiymati) {
    if (!isDirty) {
      orqaga(); // o'zgarish yo'q — serverga so'rov ham yo'q
      return;
    }
    const natija = await yoz(async () => ({
      tur: "serverdanKeldi",
      vazifa: await api.yangila(vazifa.id, qiymat),
    }));
    if (!natija.ok) {
      // Server xatosi — maydonga emas, butun formaga
      setError("root.server", { message: natija.xabar });
      return;
    }
    elonQil("O'zgarish saqlandi.");
    orqaga();
  }

Qatorma-qator:

  • qiymat: TahrirQiymati — sxemaning z.output i: kesilgan matn. Serverga " Non olish " emas, "Non olish" boradi.
  • if (!isDirty) — RHF I dagi g'oya: o'zgarish yo'q bo'lsa, so'rov ham yo'q. Tugma o'chirilmagani uchun (a11y) bu tekshiruv aynan shu yerda.
  • yoz(...) — mutatsiyalar darsidagi useYozuv: so'rov, kesh tasdig'i, xato bo'lsa { ok: false, xabar: "Saqlanmadi: …" }. Forma Query haqida hech narsa bilmaydi.
  • setError("root.server", …) — server rad etdi: xato maydonga emas, formaga. orqaga() chaqirilmaydi — mehmon sahifada qoladi va matnini yo'qotmaydi.
  • elonQil — Zustand store'idagi e'lon: ekran o'quvchi "O'zgarish saqlandi." ni eshitadi.

Root xatosi JSX'da (kanon):

tsx
      <p id="tahrir-server-xato" className="xato" role="alert">
        {errors.root?.server?.message}
      </p>

role="alert" — bu yerda status emas: server xatosi kutilmagan va muhim, ekran o'quvchi uni darhol aytsin.

7.3 Test: server rad etdi

Vitest testi (kanon, tekshiruv/VazifaTahriri.test.tsx dan bitta holat):

tsx
  test("server rad etdi — formaga xato (root), sahifada qoladi", async () => {
    server({ xato: true });
    const router = och();
    fireEvent.change(await screen.findByLabelText("Matn"), {
      target: { value: "Yangi matn" },
    });
    saqlash();
    expect(await screen.findByRole("alert")).toHaveProperty(
      "textContent",
      "Saqlanmadi: Ataylab",
    );
    expect(router.state.location.pathname).toBe("/vazifa/1/tahrir");
  });

server({ xato: true }) — fetch ning soxtasi: PATCH ga 500 va { xato: "Ataylab" } qaytaradi. Test ikki narsani tekshiradi: alert matni (useYozuv qo'shgan "Saqlanmadi: " bilan) va manzil o'zgarmagani. Testlar chuqur — RTL II darsida.

Faylning qolgan uch qismi — almashtirgich (Controller), jonli teglar (useWatch) va useOrqaga — kelgusi darsda, commit bilan.

8. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
register dagi required ishlamayapti resolver bor — register qoidalari e'tiborsiz qoidani sxemaga yozing
xato errors da yo'q, lekin forma yuborilmaydi sxemada xato path siz (obyekt refine) { path: ["maydon"] }
"Invalid input: expected string, received undefined" defaultValues berilmagan defaultValues: { name: "" }
onSubmit da hali satr ("4") sxemada coerce yo'q, yoki values emas DOM o'qildi z.coerce.number(), qiymatni data dan oling
yangi taom qo'shilgach ham "menyuda yo'q" sxema eski ro'yxatni ushlab qolgan useMemo bog'liqligiga ro'yxatni qo'shing
server xatosi hech qayerda ko'rinmaydi setError("root.server"), lekin JSX'da errors.root?.server chiqarilmagan xato <p role="alert"> ni qo'shing
errors.root.message — undefined kalit root.server, root emas errors.root?.server?.message

Yana bir eslatma. Resolver bo'lsa, register("x", { required: true }) dagi qoidalar jim e'tiborsiz qoldiriladi — ogohlantirish ham chiqmaydi. Ikkalasini aralashtirmang: qoidalar — faqat sxemada.

9. Mashqlar

Mashqlar kurs/mashqlar/19/31-resolver/ da. Har yechimni «Ishga tushir» bilan sinang.

1-mashq (oson): qaysi tur?

Sxema: z.object({ phone: z.string().transform((s) => s.replace(/\D/g, "")), guests: z.coerce.number() }).

  • defaultValues dagi phone qanday ko'rinishda? "+998 90 000 00 00" yoki "998900000000"?
  • handleSubmit(fn) da fn oladigan guests turi?
  • Resolver qaytaradigan obyektning ikki kaliti? values va
  • Butun formaga tegishli server xatosi qaysi kalitga?
Yechim
  • "+998 90 000 00 00" — defaultValues kirish turida, mehmon ko'radigan ko'rinishda.
  • number — fn chiqish turini oladi, coerce ishlagan.
  • errors — { values, errors }. Xato bo'lsa values bo'sh.
  • root.server (yoki root.<istalgan nom>). root — RHF ning maxsus kaliti.

Yechim fayli shu sxemani formaga ulab, fn dagi typeof data.guests va data.phone ni chiqaradi.

2-mashq (o'rta): qoidalarni sxemaga ko'chiring

RHF I dagi "Fikr bildiring" formasi qoidalari register ichida. Ularni Feedback sxemasiga ko'chiring va zodResolver bilan ulang:

  • name — kesiladi, bo'sh bo'lmasin: "Ismingizni yozing".
  • rating — bo'sh bo'lsa "Baho qo'ying", butun son 1–5: "Baho 1 dan 5 gacha".
  • comment — ko'pi bilan 200 belgi: "Fikr 200 belgidan oshmasin".

register larda birorta ham qoida qolmasin. Ishora: «zodResolver — formani sxemaga ulash» bo'limidagi bron formasi va Zod chuqurroq dagi preprocess.

jsx
import { useForm } from "react-hook-form";

export default function App() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    defaultValues: { name: "", rating: "", comment: "" },
  });
  return (
    <form noValidate onSubmit={handleSubmit((d) => console.log(d))}>
      <p>
        <label htmlFor="name">Ism</label>
        <input
          id="name"
          {...register("name", { required: "Ismingizni yozing" })}
        />
      </p>
      <p role="alert">{errors.name?.message}</p>
      <p>
        <label htmlFor="rating">Baho</label>
        <input
          id="rating"
          type="number"
          {...register("rating", {
            valueAsNumber: true,
            min: { value: 1, message: "Baho 1 dan 5 gacha" },
            max: { value: 5, message: "Baho 1 dan 5 gacha" },
          })}
        />
      </p>
      <p role="alert">{errors.rating?.message}</p>
      <p>
        <label htmlFor="comment">Fikr</label>
        <textarea id="comment" {...register("comment")} />
      </p>
      <button>Yuborish</button>
    </form>
  );
}

Konsolda:

text
{name: 'Malika', rating: 5, comment: ''}
Yechim
jsx
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";

const emptyToUndefined = (v) => (v === "" ? undefined : v);

const Feedback = z.object({
  name: z.string().trim().min(1, "Ismingizni yozing"),
  rating: z.preprocess(
    emptyToUndefined,
    z.coerce
      .number({ error: "Baho qo'ying" })
      .int("Baho 1 dan 5 gacha")
      .min(1, "Baho 1 dan 5 gacha")
      .max(5, "Baho 1 dan 5 gacha"),
  ),
  comment: z.string().max(200, "Fikr 200 belgidan oshmasin"),
});

export default function App() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    resolver: zodResolver(Feedback),
    defaultValues: { name: "", rating: "", comment: "" },
  });
  return (
    <form noValidate onSubmit={handleSubmit((d) => console.log(d))}>
      <p>
        <label htmlFor="name">Ism</label>
        <input id="name" {...register("name")} />
      </p>
      <p role="alert">{errors.name?.message}</p>
      <p>
        <label htmlFor="rating">Baho</label>
        <input id="rating" type="number" {...register("rating")} />
      </p>
      <p role="alert">{errors.rating?.message}</p>
      <p>
        <label htmlFor="comment">Fikr</label>
        <textarea id="comment" {...register("comment")} />
      </p>
      <p role="alert">{errors.comment?.message}</p>
      <button>Yuborish</button>
    </form>
  );
}

Konsolda:

text
{name: 'Malika', rating: 5, comment: ''}

Avval bo'sh forma yuborildi — "Ismingizni yozing" va "Baho qo'ying" chiqdi. Boshlang'ich koddagi valueAsNumber bilan bo'sh baho NaN bo'lardi va min/max uni ushlamasdi. Sxemada esa bo'sh baho o'z xabarini oldi. " Malika " — kesildi. Endi register lar faqat ulaydi, qoidalar — bitta joyda.

3-mashq (qiyin): server xatolari ikki joyda

Bron formasi (name, guests — «zodResolver — formani sxemaga ulash» bo'limidagi sxema). fakeBook(data) 200 ms dan keyin javob beradi:

  • name — "Jasur" bo'lsa: { status: 409, fieldErrors: { name: ["Bu nomga bugun bron bor"] } };
  • guests — 10 dan ko'p bo'lsa: { status: 422, fieldErrors: { guests: ["Bugun katta zal band"] } };
  • aks holda { status: 201 }.

Talablar:

  1. fieldErrors dagi har xatoni o'z maydoniga qo'ying («Server xatolari ro'yxat bo'lib kelsa» bo'limidagi sikl).
  2. 201 bo'lsa — <p role="status">Bron qabul qilindi</p>.
  3. Javob kelguncha tugmada "Yuborilmoqda…".
Yechim
jsx
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";

const emptyToUndefined = (v) => (v === "" ? undefined : v);

const Booking = z.object({
  name: z.string().trim().min(2, "Ism kamida 2 harf"),
  guests: z.preprocess(
    emptyToUndefined,
    z.coerce
      .number({ error: "Nechta mehmon?" })
      .int("Butun son yozing")
      .min(1, "Kamida 1 kishi")
      .max(12, "Ko'pi bilan 12 kishi"),
  ),
});

function fakeBook({ name, guests }) {
  const fieldErrors = {};
  if (name === "Jasur") {
    fieldErrors.name = ["Bu nomga bugun bron bor"];
  }
  if (guests > 10) fieldErrors.guests = ["Bugun katta zal band"];
  const status = Object.keys(fieldErrors).length > 0 ? 409 : 201;
  return new Promise((resolve) =>
    setTimeout(() => resolve({ status, fieldErrors }), 200),
  );
}

export default function App() {
  const {
    register,
    handleSubmit,
    setError,
    formState: { errors, isSubmitting, isSubmitSuccessful },
  } = useForm({
    resolver: zodResolver(Booking),
    defaultValues: { name: "", guests: "" },
  });

  async function book(data) {
    const res = await fakeBook(data);
    if (res.status === 201) return;
    for (const [field, messages] of Object.entries(res.fieldErrors)) {
      setError(field, { type: "server", message: messages[0] });
    }
  }

  return (
    <form noValidate onSubmit={handleSubmit(book)}>
      <p>
        <label htmlFor="name">Ism</label>
        <input id="name" {...register("name")} />
      </p>
      <p role="alert">{errors.name?.message}</p>
      <p>
        <label htmlFor="guests">Mehmonlar soni</label>
        <input id="guests" type="number" {...register("guests")} />
      </p>
      <p role="alert">{errors.guests?.message}</p>
      <button>
        {isSubmitting ? "Yuborilmoqda…" : "Bron qilish"}
      </button>
      {isSubmitSuccessful && <p role="status">Bron qabul qilindi</p>}
    </form>
  );
}

Ekranda:

text
Ism
Mehmonlar soni
Bron qilish
Bron qabul qilindi

Birinchi yuborishda ikkala server xatosi o'z maydoni ostida chiqdi (tekshiruvchi ikkalasini ko'rdi). Talabda 409 va 422 alohida edi; soxta server soddalashtirib bitta 409 qaytaradi — forma uchun muhimi fieldErrors. To'g'rilangach — "Bron qabul qilindi". isSubmitSuccessful birinchi safar false edi: setError chaqirilgan yuborish muvaffaqiyatli emas.

4-mashq: Vazifalar qadami

VazifaTahriri.tsx ga tahrirSxemasi ni useMemo va zodResolver bilan ulang, defaultValues va mode: "onTouched" bering, saqla ni «Saqlash va server xatosi» bo'limidagidek yozing va root.server xatosini chiqaring.

Yechim

Tekshiruv ro'yxati:

  • npm run dev → /vazifa/1/tahrir: matnni o'chirib, maydondan chiqing — "Vazifa matnini yozing.". Boshqa vazifaning matnini yozing — "Bu vazifa ro'yxatda allaqachon bor.".
  • Hech narsa o'zgartirmay «Saqlash» — DevTools Network'da PATCH yo'q, sahifa vazifaga qaytadi.
  • Server ishlamayotganda (mashq serverini to'xtating) — "Saqlanmadi: …" forma ostida, manzil /tahrir da qoladi.
  • Testlar va commit — RHF II darsida, almashtirgich va useOrqaga bilan birga.

10. Real ishda

  • Standart juftlik. "React Hook Form + Zod" — React loyihalaridagi eng keng tarqalgan forma to'plami. shadcn/ui'ning forma qo'llanmasi ham aynan shu ikkisini ulaydi (UI kutubxonalar xaritasi).
  • Boshqa resolverlar. @hookform/resolvers da Valibot, Yup, ArkType va Standard Schema uchun ham resolver bor — forma kodi o'zgarmaydi, faqat import (Muqobillar).
  • Monorepo'da sxemalar paketi — tRPC va Next.js loyihalarida sxema ko'pincha server va klient o'rtasidagi "shartnoma": API'ning kirish turi ham shu sxemadan chiqadi (GraphQL va tRPC).
  • Intervyu savollari: "resolver nima qiladi?", "defaultValues va onSubmit dagi turlar nega farq qiladi?", "server xatosini formada qanday ko'rsatasiz?", "brauzerdagi Zod tekshiruvi serverni almashtiradimi?".

Xulosa

  • zodResolver(schema) — qoidalar faqat sxemada; register faqat ulaydi. Resolver — { values, errors } qaytaradigan funksiya.
  • defaultValues va maydonlar — z.input, handleSubmit dagi data — z.output (kesilgan, songa aylangan).
  • Ma'lumotga bog'liq qoida — sxema fabrikasi + useMemo; brauzer tekshiruvi — tez javob, kafolat — server.
  • Server xatosi: maydonga — setError("name", …), butun formaga — setError("root.server", …); root faqat keyingi yuborishda tozalanadi.
  • Sxema alohida faylda (yoki paketda): brauzer, server va testlar bitta qoidani ishlatadi.

Keyingi dars: RHF II: Controller, watch va tahrirlash formasi — maxsus komponentlarni Controller bilan ulash, useWatch bilan jonli ko'rinish, reset va dirtyFields; vazifalar tahrir formasini tugatib, commit.

Manbalar

  • React Hook Form: useForm — resolver, defaultValues, setError — react-hook-form.com/docs/useform (2026-10 holatiga, v7.89.0)
  • @hookform/resolvers (5.9.1) — github.com/react-hook-form/resolvers
  • Zod 4: Formatting errors (z.flattenError) — zod.dev/error-formatting
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
React Hook Form va Zod: zodResolver, umumiy sxema va server xatolari — IlmHamroh