Mundarija (30)
- Bu darsda
- 1. Nega bu kerak?
- 2. zodResolver — formani sxemaga ulash
- 2.1 Birinchi ulanish
- 2.2 Resolver nima qiladi?
- 2.3 Resolver qachon chaqiriladi?
- 3. Kirish va chiqish turlari formada
- 3.1 defaultValues — kirish turida
- 3.2 Nega defaultValues doim kerak
- 4. Ma'lumotga bog'liq sxema
- 4.1 Sxema fabrikasi
- 5. Server xatolari: setError
- 5.1 Maydonga va butun formaga
- 5.2 Server xatolari ro'yxat bo'lib kelsa
- 6. Umumiy sxemalar: brauzer va server
- 6.1 Sxema — alohida faylda
- 6.2 Nimani ulashmaslik kerak
- 7. Vazifalar qadami: sxema formaga ulandi (holat 32)
- 7.1 zodResolver va defaultValues
- 7.2 Saqlash va server xatosi
- 7.3 Test: server rad etdi
- 8. Ko'p uchraydigan xatolar
- 9. Mashqlar
- 1-mashq (oson): qaysi tur?
- 2-mashq (o'rta): qoidalarni sxemaga ko'chiring
- 3-mashq (qiyin): server xatolari ikki joyda
- 4-mashq: Vazifalar qadami
- 10. Real ishda
- Xulosa
- Manbalar
React Hook Form va Zod: zodResolver, umumiy sxema va server xatolari
Qisqacha:
useForm({ resolver: zodResolver(schema) })— formaning hamma qoidalari endi bitta Zod sxemasida,registerda qoida yozilmaydi. Resolver sxema xatolarinierrors.<maydon>.messagega joylaydi,handleSubmitesa sxemaning chiqish qiymatini (kesilgan matn, son) beradi.defaultValues— sxemaning kirish turida. Server rad etsa —setError("maydon", …)yoki butun formagasetError("root.server", …). Sxema alohida faylda tursa, uni server ham ishlatadi.
Bu darsda
@hookform/resolvers/zodbilan formani sxemaga ulaysiz va resolver ichida nima bo'lishini o'z resolveringizni yozib ko'rasiz.- Formada
z.input(maydonlar) vaz.output(yuborish) farqini ishlatasiz,defaultValuesni to'g'ri berasiz. - Ma'lumotga bog'liq sxemani (takror tekshiruvi) "sxema fabrikasi" va
useMemobilan yasaysiz. - Server xatolarini maydonga va butun formaga
setErrorbilan 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:
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:
{name: 'Malika', guests: 4}Ekranda:
Ism
Mehmonlar soni
Bron qilishAvval 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:handleSubmitsxemaning chiqish qiymatini berdi.guests: 4— son.valueAsNumberyozmadik,coerceishladi.
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:
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:
resolver: {name: 'A'}Ekranda:
Ism
Ism kamida 2 harf
Bron qilishYigirma qatorda zodResolver ning asosiy ishi:
- qiymatlarni sxemadan o'tkazish (
safeParse); - har xatoning
pathini maydon nomiga aylantirish (["items", 2, "dish"]→"items.2.dish"); - 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 vaonSubmitnima 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):
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:
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):
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:
isDirty— "boshlang'ich qiymatdan farq" — boshlang'ichsiz ma'nosiz (RHF I).- Turlar. TypeScript formaning shaklini
defaultValuesdan ham oladi. undefinedemas,"". Boshlang'ichsiz maydonundefinedbilan boshlanadi. Sxemadaz.string()bo'lsa, xato matni "expected string, received undefined" bo'ladi — mehmon uchun tushunarsiz.- 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:
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:
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 testdadishSchema(() => true)kabi chaqirib, React'siz sinash mumkin.useMemo— sxema faqatmenuo'zgarganda qayta yasaladi (useMemo). Har renderda yangi sxema ham ishlardi, lekinzodResolver(schema)ham har safar yangi funksiya bo'lardi — keraksiz ish.- RHF yangi resolver'ni oladi.
useFormhar renderda berilganresolverni 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:
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:
Taom nomi
Saqlanmadi (server: 500)
Muvaffaqiyat: false
Qo'shishIkki 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:
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:
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" --> WBitta 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
fieldErrorsshaklida 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):
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 uchunresetyoki asyncdefaultValueskerak emas.
7.2 Saqlash va server xatosi
saqla — handleSubmit ga beriladigan funksiya (kanon):
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— sxemaningz.outputi: 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 darsidagiuseYozuv: 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):
<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):
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() }).
defaultValuesdagiphoneqanday ko'rinishda?"+998 90 000 00 00"yoki"998900000000"?handleSubmit(fn)dafnoladigangueststuri?- Resolver qaytaradigan obyektning ikki kaliti?
valuesva - Butun formaga tegishli server xatosi qaysi kalitga?
Yechim
"+998 90 000 00 00"—defaultValueskirish turida, mehmon ko'radigan ko'rinishda.number—fnchiqish turini oladi,coerceishlagan.errors—{ values, errors }. Xato bo'lsavaluesbo'sh.root.server(yokiroot.<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.
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:
{name: 'Malika', rating: 5, comment: ''}Yechim
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:
{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:
fieldErrorsdagi har xatoni o'z maydoniga qo'ying («Server xatolari ro'yxat bo'lib kelsa» bo'limidagi sikl).- 201 bo'lsa —
<p role="status">Bron qabul qilindi</p>. - Javob kelguncha tugmada "Yuborilmoqda…".
Yechim
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:
Ism
Mehmonlar soni
Bron qilish
Bron qabul qilindiBirinchi 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
/tahrirda qoladi. - Testlar va commit — RHF II darsida, almashtirgich va
useOrqagabilan 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/resolversda 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?", "
defaultValuesvaonSubmitdagi turlar nega farq qiladi?", "server xatosini formada qanday ko'rsatasiz?", "brauzerdagi Zod tekshiruvi serverni almashtiradimi?".
Xulosa
zodResolver(schema)— qoidalar faqat sxemada;registerfaqat ulaydi. Resolver —{ values, errors }qaytaradigan funksiya.defaultValuesva maydonlar —z.input,handleSubmitdagidata—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", …);rootfaqat 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
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!