Mundarija (31)
- Bu darsda
- 1. Nega bu kerak?
- 2. useFieldArray — dinamik maydonlar
- 2.1 Qo'shish va o'chirish
- 2.2 Nega key={field.id}, indeks emas
- 2.3 Massivga qoidalar
- 3. Ko'p qadamli forma (wizard)
- 3.1 Bitta forma, bir necha qadam
- 3.2 Fokus va a11y
- 4. Async validatsiya
- 4.1 Serverdan so'raladigan qoida
- 4.2 So'rovni kamaytirish
- 5. Saqlanmagan o'zgarishlar
- 5.1 Ilova ichida: useBlocker
- 5.2 Tab yopilganda: beforeunload
- 5.3 Avtosaqlash: qoralama
- 6. Vazifalar qadami: «Qadamlarga bo'lib qo'shish» (holat 33)
- 6.1 Nima quramiz
- 6.2 Sxema: sxemalar/loyiha-formasi.ts
- 6.3 Forma: sahifalar/YangiLoyiha.tsx
- 6.4 Marshrut va havola
- 6.5 Tekshiruvlar va commit
- 7. Ko'p uchraydigan xatolar
- 8. Mashqlar
- 1-mashq (oson): qaysi biri?
- 2-mashq (o'rta): telefon raqamlari ro'yxati
- 3-mashq (qiyin): taom tahriri — chiqishdan himoya
- 4-mashq: Vazifalar qadami
- 9. Real ishda
- Xulosa
- Manbalar
React Hook Form useFieldArray, ko'p qadamli forma va async validatsiya
Qisqacha: Ro'yxat ko'rinishidagi maydonlar —
useFieldArray:fields(har birida barqarorid—keyuchun),append,remove,move. Ko'p qadamli forma (wizard) — bittauseForm, har "Keyingi" da faqat shu qadam maydonlaritrigger([...])bilan tekshiriladi. Serverda tekshiriladigan qoida — asyncvalidateyoki asyncsuperRefine; har harfda so'rov ketmasligi uchunmode/reValidateModeyoki debounce. Saqlanmagan o'zgarish —useBlocker(ilova ichida) vabeforeunload(tab yopilganda); qoralama — avtosaqlash.
Bu darsda
useFieldArraybilan qo'shiladigan va o'chiriladigan maydonlar yozasiz vakey={field.id}nega shartligini tushunasiz.- Ko'p qadamli formani bitta
useFormvatriggerbilan quraysiz, qadam almashganda fokusni boshqarasiz. - Async tekshiruvni yozasiz va so'rovlar sonini
modeva debounce bilan kamaytirasiz. - Saqlanmagan o'zgarishni
useBlockervabeforeunloadbilan himoya qilasiz, qoralamani avtomatik saqlaysiz. vazifalarga «Qadamlarga bo'lib qo'shish» formasini qo'shasiz.
Oldin bilishingiz kerak: RHF II: Controller, watch va tahrirlash formasi, Zod chuqurroq, Debounce va throttle, React Router IV: data mode.
1. Nega bu kerak?
Sardor uyni ta'mirlashni rejalashtiryapti: "Bo'yoq olish #ta'mir", "Devorni tozalash #ta'mir", "Bo'yash #ta'mir"... vazifalar ga har birini alohida yozish va har safar tegni takrorlash zerikarli. U bitta formani xohlaydi: avval teg, keyin qadamlar ro'yxati (nechta bo'lsa shuncha), oxirida ko'rib chiqish — va hammasi bir bosishda qo'shilsin.
Bu bitta formada uchta yangi muammo bor:
- Maydonlar soni oldindan noma'lum. Uch qadam ham, o'n qadam ham bo'lishi mumkin. Har biriga
register("qadam1"),register("qadam2")deb yozib bo'lmaydi. - Forma bir necha bosqichda. Mehmon tegni yozadi, "Keyingi" ni bosadi — faqat teg tekshirilishi kerak, hali yozilmagan qadamlar emas.
- Ba'zi qoidalarni faqat server biladi. "Bo'yoq olish #ta'mir" ro'yxatda allaqachon bormi? Boshqa qurilmada kimdir hozirgina qo'shgan bo'lishi mumkin — so'rash kerak.
Hayotiy o'xshatish: bank kartasini rasmiylashtirish. Xodim hujjatlarni bosqichma-bosqich so'raydi: avval pasport, keyin manzil, keyin imzo. Har bosqichda faqat shu bosqich tekshiriladi. Oila a'zolari ro'yxati — nechta bo'lsa shuncha qator. Oxirida esa "bu raqam bazada bormi?" degan savol markaziy idoraga yuboriladi.
2. useFieldArray — dinamik maydonlar
2.1 Qo'shish va o'chirish
Buyurtmada taomlar ro'yxati: har qatorda taom nomi va soni. useFieldArray({ control, name }) massiv ko'rinishidagi maydon bilan ishlaydi:
import { useFieldArray, useForm } from "react-hook-form";
export default function App() {
const { register, control, handleSubmit } = useForm({
defaultValues: { items: [{ dish: "Osh", qty: 2 }] },
});
const { fields, append, remove } = useFieldArray({
control,
name: "items",
});
return (
<form onSubmit={handleSubmit((d) => console.log(d.items))}>
<ol>
{fields.map((field, i) => (
<li key={field.id}>
<input
id={`dish-${i}`}
aria-label={`${i + 1}-taom`}
{...register(`items.${i}.dish`)}
/>
<input
id={`qty-${i}`}
type="number"
aria-label={`${i + 1}-taom soni`}
{...register(`items.${i}.qty`, { valueAsNumber: true })}
/>
<button
type="button"
aria-label={`O'chirish: ${i + 1}-taom`}
onClick={() => remove(i)}
>
✕
</button>
</li>
))}
</ol>
<button
type="button"
onClick={() => append({ dish: "", qty: 1 })}
>
Taom qo'shish
</button>
<button>Buyurtma</button>
</form>
);
}Konsolda:
(2) [{dish: 'Manti', qty: 3}, {dish: '', qty: 1}]Ketma-ketlik: boshida bitta qator (osh). "Taom qo'shish" — ikkinchi qator, unga "Manti, 3". Yana bitta qo'shildi — uchinchi, bo'sh. Keyin birinchi qator (osh) o'chirildi. Natijada ikki qator: manti va bo'sh qator.
Asosiy vositalar:
| Metod | Nima qiladi |
|---|---|
fields |
ko'rsatish uchun massiv; har elementda id |
append(obj) / prepend(obj) |
oxiriga / boshiga qo'shadi |
insert(i, obj) |
i o'rniga qo'yadi |
remove(i) |
i ni o'chiradi (remove() — hammasini) |
move(a, b) / swap(a, b) |
joyini almashtiradi |
update(i, obj) / replace(arr) |
qiymatni almashtiradi |
Maydon nomi — nuqtali yo'l: items.0.dish, items.1.qty. Sxemadagi path ham shunday (Zod chuqurroq dagi ["dishes", 2]).
append dan keyin RHF fokusni yangi qatorning birinchi maydoniga o'tkazadi (shouldFocus: true — sukut). Bobur yangi qatorni Tab bilan izlamaydi. Kerak bo'lmasa — append(obj, { shouldFocus: false }).
2.2 Nega key={field.id}, indeks emas
fields[i].id — RHF bergan barqaror kalit. Indeks (key={i}) bilan nima bo'lishini key darsidan bilasiz: o'rtadagi qator o'chirilsa, keyingilari bitta yuqoriga suriladi. React esa "2-qator o'chdi" emas, "oxirgi qator o'chdi, qolganlariga yangi props keldi" deb o'ylaydi. Har qatorning ichki holati (masalan, ochilgan "izoh" maydoni yoki maxsus komponentning state'i) noto'g'ri qatorga ko'chadi.
field.id qiymat bilan birga ko'chadi: o'chirilgan qatorning komponenti o'chadi, qolganlari o'z holatini saqlaydi.
Diqqat:
fields— ko'rsatish uchun, qiymatlar uchun emas. Unda boshlang'ich qiymatlar turadi: mehmon maydonga yozgandafields[i].dishyangilanmaydi. Joriy qiymat kerak bo'lsa —useWatch({ control, name: "items" })yokigetValues("items")(RHF II).
2.3 Massivga qoidalar
Massivning o'ziga ham qoida bo'ladi: "kamida bitta taom", "ko'pi bilan 10 ta". Sxemada z.array(...).min(1).max(10). Bunday xato massivga tegishli, biror qatorga emas. RHF uni errors.items.root (yoki eski holatda errors.items.message) ga qo'yadi. Har qatorning o'z xatosi esa — errors.items[i].dish. Zod chuqurroq dagi 3-mashq (takror taom, jami porsiya) aynan shu formaning sxemasi edi.
Tekshirib ko'ring: Uch qatorli ro'yxatda o'rtadagisi o'chirildi.
registernomlari (items.0,items.1,items.2) bilan nima bo'ladi?
Javob
Nomlar indeksga bog'liq, shuning uchun qayta hisoblanadi: uchinchi qator endi items.1 bo'ladi. Buni RHF o'zi qiladi — qiymatlar massivdan o'chadi va qolganlari suriladi. Sizdan talab bitta: nomni har renderda items.${i} deb indeks bilan yozing, key ga esa field.id bering. Nom — "qaysi o'rinda", kalit — "qaysi qator".
3. Ko'p qadamli forma (wizard)
3.1 Bitta forma, bir necha qadam
Ko'p qadamli formani ikki usulda qurish mumkin:
- Har qadam — alohida
useForm. Har biri o'z sxemasi bilan, natijalar umumiy state'da yig'iladi. Qadamlar mustaqil bo'lsa qulay, lekin qadamlar orasidagi qoidalar (teg + qadam uzunligi) qiyinlashadi. - Bitta
useForm, qadamlar — ko'rinish. Hamma maydonlar bitta formada, faqat hozirgi qadamniki ko'rinadi. "Keyingi" bosilgandatrigger([...nomlar])— faqat shu maydonlarni tekshiradi vatrue/falseqaytaradi.
vazifalar ikkinchisini tanlagan. Qadam o'zgarganda oldingi maydonlar ekrandan yo'qoladi (unmount), lekin qiymatlari formada qoladi — shouldUnregister: false (sukut). Uch qadamli bron:
import { useState } from "react";
import { useForm } from "react-hook-form";
const STEPS = ["Ism", "Mehmonlar", "Tasdiq"];
const FIELDS = [["name"], ["guests"], []];
export default function App() {
const [step, setStep] = useState(0);
const {
register,
trigger,
getValues,
formState: { errors },
} = useForm({ defaultValues: { name: "", guests: "" } });
async function next() {
if (await trigger(FIELDS[step], { shouldFocus: true })) {
setStep(step + 1);
}
}
return (
<form noValidate onSubmit={(e) => e.preventDefault()}>
<p>
{step + 1}/{STEPS.length}: {STEPS[step]}
</p>
{step === 0 && (
<p>
<label htmlFor="name">Ism</label>
<input
id="name"
{...register("name", { required: "Ismingizni yozing" })}
/>
<span role="alert"> {errors.name?.message}</span>
</p>
)}
{step === 1 && (
<p>
<label htmlFor="guests">Mehmonlar soni</label>
<input
id="guests"
type="number"
{...register("guests", { required: "Nechta mehmon?" })}
/>
</p>
)}
{step === 2 && (
<p>
{getValues("name")}, {getValues("guests")} kishi —
tasdiqlaysizmi?
</p>
)}
{step < 2 && (
<button type="button" onClick={() => void next()}>
Keyingi
</button>
)}
</form>
);
}Ekranda:
3/3: Tasdiq
Malika, 4 kishi — tasdiqlaysizmi?Birinchi "Keyingi" bo'sh ism bilan — xato chiqdi, qadam o'zgarmadi (trigger false qaytardi). Ism yozildi — ikkinchi qadamga o'tdi. Uchinchi qadamda ism va son hali bor, garchi ularning maydonlari allaqachon ekrandan ketgan bo'lsa ham.
FIELDS — har qadamda nima tekshirilishi. Uchinchi qadamda bo'sh massiv: tekshiradigan maydon yo'q. Agar trigger() argumentsiz chaqirilsa — hamma maydon tekshiriladi, shu jumladan hali ko'rinmaganlar ham. Wizard'da bu xato.
stateDiagram-v2
[*] --> Teg
Teg --> Qadamlar: Keyingi (trigger teg)
Qadamlar --> Teg: Orqaga
Qadamlar --> Tekshirish: Keyingi (trigger qadamlar)
Tekshirish --> Qadamlar: Orqaga
Tekshirish --> [*]: Yaratish (butun sxema)3.2 Fokus va a11y
Qadam almashganda eski maydonlar yo'qoladi, fokus esa... hech qayerda. Ekran o'quvchi Malika qayerda ekanini bilmaydi. Yechim — qadam sarlavhasiga fokus berish. vazifalar da bu <legend tabIndex={-1}> va useLayoutEffect (pastda, «Vazifalar qadami»da). Qoida Fokus boshqaruvi darsidan: ekranning katta qismi almashganda — fokusni yangi qismning sarlavhasiga.
Yana ikki odat:
- Qadamlar ro'yxati (
<ol>) va joriy qadamdaaria-current="step"— "3 qadamdan 2-chisi" ko'rinadi va eshitiladi. - «Orqaga» tugmasi qiymatlarni yo'qotmaydi — forma bitta. Lekin orqaga qaytganda tekshirish kerak emas: mehmon tuzatish uchun qaytgan.
Tekshirib ko'ring: Wizard'da
useForm({ shouldUnregister: true })qo'yilsa, uchinchi qadamdagetValues("name")nima beradi?
Javob
undefined. shouldUnregister: true — maydon ekrandan ketganda (unmount) uning qiymati ham formadan o'chiriladi. Wizard'da qadam maydonlari aynan shunday ketadi, demak oxirgi qadamga yetganda faqat oxirgi qadam qiymatlari qoladi. Sukut — false, wizard uchun aynan shu kerak.
4. Async validatsiya
4.1 Serverdan so'raladigan qoida
"Bu nom serverda bormi?" — sxema bilmaydi, server biladi. RHF da ikki joy bor: register dagi async validate yoki sxemadagi async refine / superRefine (unda resolver o'zi safeParseAsync ni ishlatadi). Avval validate ni sinaymiz — mode: "onChange" bilan, soxta server 200 ms da javob beradi va har so'rovni sanaydi:
import { useForm } from "react-hook-form";
const TAKEN = ["ali", "vali"];
let requests = 0;
function isTaken(login) {
requests += 1;
console.log(`so'rov ${requests}: ${login}`);
return new Promise((resolve) =>
setTimeout(() => resolve(TAKEN.includes(login)), 200),
);
}
export default function App() {
const {
register,
formState: { errors, isValidating },
} = useForm({ mode: "onChange", defaultValues: { login: "" } });
return (
<form>
<p>
<label htmlFor="login">Login</label>
<input
id="login"
{...register("login", {
validate: async (v) =>
!(await isTaken(v)) || "Bu login band",
})}
/>
</p>
<p role="status">
{isValidating ? "Tekshirilmoqda…" : errors.login?.message}
</p>
</form>
);
}Konsolda:
so'rov 1: a
so'rov 2: al
so'rov 3: aliEkranda:
Login
Bu login bandUch harf — uch so'rov. Har harfda serverga. 1 000 foydalanuvchi bir vaqtda ro'yxatdan o'tsa — har biri o'nlab so'rov. Bundan tashqari, javoblar tartibsiz kelishi mumkin: "al" ning javobi "ali" nikidan keyin kelsa, eskisi yangisining ustiga yozadi. isValidating — tekshiruv davom etayotgan payt (isSubmitting ning tekshiruvdagi juftligi).
4.2 So'rovni kamaytirish
Uch yo'l bor, eng soddasidan:
modeni o'zgartirish.mode: "onBlur"yoki"onSubmit"vareValidateMode: "onBlur"— tekshiruv maydondan chiqqanda yoki yuborishda. Bitta maydon — bitta so'rov.vazifalaraynan shu yo'lni tanlagan.- Debounce. Mehmon yozishni to'xtatgach (masalan, 400 ms) bir marta so'rash (Debounce). "Login bo'sh" kabi jonli maslahat kerak bo'lsa.
- Kesh. Bir marta tekshirilgan qiymatni qayta so'ramaslik —
Mapyoki TanStack Query (kesh).
mode: "onBlur" bilan o'sha forma (tugma fokusni keyingi maydonga o'tkazadi — Tab o'rnida, RHF I dagidek):
import { useForm } from "react-hook-form";
const TAKEN = ["ali", "vali"];
let requests = 0;
function isTaken(login) {
requests += 1;
console.log(`so'rov ${requests}: ${login}`);
return new Promise((resolve) =>
setTimeout(() => resolve(TAKEN.includes(login)), 200),
);
}
export default function App() {
const {
register,
formState: { errors },
} = useForm({ mode: "onBlur", defaultValues: { login: "" } });
return (
<form>
<p>
<label htmlFor="login">Login</label>
<input
id="login"
autoFocus
{...register("login", {
validate: async (v) =>
!(await isTaken(v)) || "Bu login band",
})}
/>
</p>
<p>
<label htmlFor="name">Ism</label>
<input id="name" />
</p>
<p role="status">{errors.login?.message}</p>
<button
type="button"
onClick={() => document.getElementById("name").focus()}
>
Keyingi maydon
</button>
</form>
);
}Konsolda:
so'rov 1: aliEkranda:
Login
Ism
Bu login band
Keyingi maydonUch harf — bitta so'rov, yozib bo'lingan login bilan.
Diqqat: Brauzerdagi "band emas" — kafolat emas. Siz "Keyingi" ni bosgan soniyada boshqa odam o'sha loginni olishi mumkin. Haqiqiy tekshiruv — yuborishda, serverda (ma'lumotlar bazasidagi noyoblik sharti). Server rad etsa — RHF + Zod dagi
setError.
5. Saqlanmagan o'zgarishlar
5.1 Ilova ichida: useBlocker
Mehmon uzun formani yarim to'ldirdi va menyudagi havolani bosdi — hammasi yo'qoldi. React Router'da useBlocker(shart) navigatsiyani to'xtatadi. U blocker qaytaradi: state — "blocked" bo'lsa, foydalanuvchidan so'raymiz, keyin blocker.proceed() (ketish) yoki blocker.reset() (qolish). Shart — RHF ning isDirty i:
import { useForm } from "react-hook-form";
import {
createMemoryRouter,
Link,
RouterProvider,
useBlocker,
} from "react-router";
function EditDish() {
const {
register,
formState: { isDirty },
} = useForm({ defaultValues: { price: "35000" } });
const blocker = useBlocker(isDirty);
return (
<form>
<p>
<label htmlFor="price">Narxi</label>
<input id="price" {...register("price")} />
</p>
<Link to="/">Menyu</Link>
{blocker.state === "blocked" && (
<div role="alertdialog" aria-label="Chiqishni tasdiqlang">
<p>Saqlanmagan o'zgarish bor. Chiqasizmi?</p>
<button type="button" onClick={() => blocker.proceed()}>
Chiqish
</button>
<button type="button" onClick={() => blocker.reset()}>
Qolish
</button>
</div>
)}
</form>
);
}
const router = createMemoryRouter(
[
{ path: "/", element: <Link to="/osh">Tahrirlash</Link> },
{ path: "/osh", element: <EditDish /> },
],
{ initialEntries: ["/"] },
);
export default function App() {
return <RouterProvider router={router} />;
}Ekranda:
Narxi
MenyuNarx o'zgardi → "Menyu" bosildi → router to'xtadi va so'radi (tekshiruvchi oynani ko'rdi) → "Qolish" → forma joyida, kiritilgan narx bilan. "Chiqish" bosilganda esa navigatsiya davom etadi.
Natija oynasida manzil satri yo'q, shuning uchun xotiradagi router — loyihada createBrowserRouter, useBlocker bir xil ishlaydi. useBlocker faqat data router (data mode) bilan ishlaydi — <BrowserRouter> bilan emas.
window.confirm dan ko'ra o'z oynangiz yaxshiroq: matnni siz boshqarasiz va u a11y talablariga mos. Haqiqiy loyihada bu <dialog> bo'ladi (dialog).
5.2 Tab yopilganda: beforeunload
useBlocker faqat ilova ichidagi navigatsiyani ko'radi. Tab yopilsa, sahifa yangilansa yoki boshqa saytga o'tilsa — bu brauzerning ishi. Buning uchun beforeunload hodisasi:
useEffect(() => {
if (!isDirty) return;
function warn(event) {
event.preventDefault(); // brauzer o'z oynasini ko'rsatadi
}
window.addEventListener("beforeunload", warn);
return () => window.removeEventListener("beforeunload", warn);
}, [isDirty]);React Router'da bunga tayyor hook bor: useBeforeUnload(fn). Uch narsani bilish kerak:
- Brauzer o'z matnini ko'rsatadi ("Saytdan chiqasizmi? O'zgarishlar saqlanmasligi mumkin"). O'z matningizni berib bo'lmaydi — bu firibgarlikka qarshi cheklov.
- Hodisa faqat o'zgarish bor paytda ulansin (
if (!isDirty) return). Doim ulanganbeforeunloadbrauzerning orqaga-oldinga keshini (bfcache) buzadi — sahifa qaytganda tez ochilmaydi. - Bu blok saytda «Ishga tushir» siz: natija oynasini "yopib" bo'lmaydi. O'z loyihangizda sinang.
5.3 Avtosaqlash: qoralama
Ogohlantirishdan ham yaxshisi — hech narsa yo'qolmasin. Avtosaqlash: mehmon yozishni to'xtatgach, qoralama localStorage ga yoziladi, keyingi ochilishda defaultValues ga qaytadi. RHF ning watch(callback) i qayta chizmasdan har o'zgarishda funksiya chaqiradi (RHF II), debounce — setTimeout:
import { useEffect } from "react";
import { useForm } from "react-hook-form";
const KEY = "bron:qoralama";
function loadDraft() {
try {
return JSON.parse(localStorage.getItem(KEY)) ?? { note: "" };
} catch {
return { note: "" };
}
}
export default function App() {
const { register, watch } = useForm({ defaultValues: loadDraft() });
useEffect(() => {
let timer;
const subscription = watch((values) => {
clearTimeout(timer);
timer = setTimeout(() => {
localStorage.setItem(KEY, JSON.stringify(values));
console.log("qoralama saqlandi:", values.note);
}, 400);
});
return () => {
clearTimeout(timer);
subscription.unsubscribe();
};
}, [watch]);
return (
<form>
<label htmlFor="note">Izoh</label>
<textarea id="note" {...register("note")} />
</form>
);
}Konsolda:
qoralama saqlandi: Deraza yonidaIkki o'zgarish — bitta yozuv: ikkinchi o'zgarish birinchisining taymerini bekor qildi. Obuna (watch(callback)) effektda ulanadi. Effekt tozalanganda u unsubscribe qilinadi (useEffect I). Natija oynasida localStorage ham ishlaydi: u soxta nusxa va sahifa yangilanganda tozalanadi. Loyihada esa haqiqiy.
Qoralama — shu qurilmada. Yuborilgandan keyin uni o'chiring (localStorage.removeItem(KEY)), aks holda keyingi ochilishda eski matn chiqadi. Parol, karta raqami kabi maydonlar qoralamaga yozilmaydi.
6. Vazifalar qadami: «Qadamlarga bo'lib qo'shish» (holat 33)
6.1 Nima quramiz
Yangi sahifa /yangi: uch qadam — "Teg", "Qadamlar", "Tekshirish". Natija — bir nechta vazifa, hammasi bitta teg bilan: "Bo'yoq olish #ta'mir", "Bo'yash #ta'mir". Ro'yxat sahifasidagi tezkor forma ostida havola: "Katta ishni qadamlarga bo'lib qo'shish…".
git switch -c feat/qadamlarga-bolish6.2 Sxema: sxemalar/loyiha-formasi.ts
Kanon (to'liq; izoh qatorlari telefonga sig'ishi uchun bo'lingan):
// loyiha-formasi.ts — "Qadamlarga bo'lib qo'shish" ustasi (wizard,
// 19/#33) sxemasi. Bitta forma, uch qadam: har qadam o'z
// maydonlarini tekshiradi (trigger), oxirida — butun sxema. Natija —
// bir nechta vazifa: har qadam alohida vazifa, hammasi bitta teg
// bilan
import { z } from "zod/mini";
import { Vazifa } from "../vazifa.ts";
export const QADAM_ENG_KOP = 10;
// Teg (12/#08 qoidasi): # bilan, harf/raqam/_ va ichida apostrof
const TEG = /^#[\p{L}\p{N}_]+(?:['‘’][\p{L}\p{N}_]+)*$/u;
/** Vazifa matni: "<qadam> <teg>" */
export function vazifaMatni(qadam: string, teg: string): string {
return `${qadam.trim()} ${teg.trim()}`;
}
/**
* serverdaBormi — ASYNC tekshiruv (19/#33): serverdagi ro'yxatda shu
* matnli vazifa bormi. Sxema serverni bilmaydi — funksiya tashqaridan
*/
export function loyihaSxemasi(
serverdaBormi: (matnlar: readonly string[]) => Promise<string | null>,
) {
const qadam = z.object({
matn: z
.string()
.check(
z.trim(),
z.minLength(1, "Qadamni yozing yoki olib tashlang."),
),
});
return z
.object({
teg: z
.string()
.check(
z.trim(),
z.regex(TEG, "Teg # bilan boshlansin, bo'shliqsiz: #ta'mir."),
),
qadamlar: z
.array(qadam)
.check(
z.minLength(1, "Kamida bitta qadam."),
z.maxLength(
QADAM_ENG_KOP,
`Ko'pi bilan ${QADAM_ENG_KOP} qadam.`,
),
),
})
.check(
// Butun forma darajasidagi qoidalar: matn uzunligi va takror
// (teg bilan birga hisoblanadi) — xato tegishli qadam maydoniga
z.superRefine(async ({ teg, qadamlar }, ctx) => {
const matnlar = qadamlar.map((q) => vazifaMatni(q.matn, teg));
matnlar.forEach((matn, i) => {
if (matn.length > Vazifa.MAKS_UZUNLIK) {
ctx.issues.push({
code: "custom",
input: matn,
path: ["qadamlar", i, "matn"],
message: `Teg bilan ${Vazifa.MAKS_UZUNLIK} belgidan oshdi.`,
});
}
if (matnlar.indexOf(matn) !== i) {
ctx.issues.push({
code: "custom",
input: matn,
path: ["qadamlar", i, "matn"],
message: "Bu qadam yuqorida bor.",
});
}
});
if (ctx.issues.length > 0) {
return;
}
const bor = await serverdaBormi(matnlar);
if (bor !== null) {
ctx.issues.push({
code: "custom",
input: bor,
path: ["qadamlar", matnlar.indexOf(bor), "matn"],
message: `«${bor}» ro'yxatda allaqachon bor.`,
});
}
}),
);
}
export type LoyihaKirishi = z.input<ReturnType<typeof loyihaSxemasi>>;
export type LoyihaQiymati = z.output<ReturnType<typeof loyihaSxemasi>>;To'rt darsning g'oyalari bitta faylda:
- Teg qoidasi — ro'yxatdagi teglar bilan bir xil muntazam ifoda (12-qism dagi Unicode
ubayrog'i):#ta'mirham,#oʻquvham. z.array(qadam).check(minLength, maxLength)— massivga qoida. Xatoerrors.qadamlar.rootga tushadi.superRefine— butun forma darajasida. "Teg bilan 100 belgidan oshdi" faqat teg va qadam birga ma'lum bo'lganda hisoblanadi. Xatopath: ["qadamlar", i, "matn"]— aynan o'sha qator ostida.- Async va arzon tekshiruv tartibi. Avval mahalliy qoidalar (uzunlik, takror); xato bo'lsa —
return, serverga so'rov yo'q. Faqat hammasi to'g'ri bo'lsa —await serverdaBormi(...). Arzon tekshiruv qimmatidan oldin. z.inputvaz.output— ikkala tur eksport qilinadi, forma ularniuseForm<Kirish, unknown, Chiqish>ga beradi (RHF + Zod).
6.3 Forma: sahifalar/YangiLoyiha.tsx
Fayl 270 qator — asosiy qismlari (kanon). Boshidagi izoh:
// YangiLoyiha.tsx — "/yangi" (19/#33): ko'p qadamli forma (wizard).
// BITTA useForm, uch qadam: qadam o'zgarganda maydonlar unmount
// bo'ladi, lekin qiymatlar formada qoladi (shouldUnregister: false —
// sukut). Har "Keyingi" — faqat shu qadam maydonlari tekshiriladi
// (trigger). Dinamik maydonlar — useFieldArray; serverdagi takror —
// async tekshiruvuseForm va async tekshiruv:
const QADAMLAR = ["Teg", "Qadamlar", "Tekshirish"] as const;
// Har qadamda qaysi maydonlar tekshiriladi
const MAYDONLAR: readonly (readonly FieldPath<LoyihaKirishi>[])[] = [
["teg"],
["qadamlar"],
[],
];
// ...
} = useForm<LoyihaKirishi, unknown, LoyihaQiymati>({
// Async tekshiruv (19/#33): serverdagi ENG SO'NGGI ro'yxat
// (keshda eskirgan bo'lishi mumkin — staleTime 0 bilan qayta
// so'raladi)
resolver: zodResolver(
loyihaSxemasi(async (matnlar) => {
const royxat = VazifalarRoyxati.dan(
await mijoz.fetchQuery({
...royxatSorovi(mijoz),
staleTime: 0,
}),
);
return matnlar.find((m) => royxat.matnBormi(m, null)) ?? null;
}),
),
defaultValues: { teg: "", qadamlar: [{ matn: "" }] },
// Tekshiruv — "Keyingi" va yuborishda; xato bo'lgach — maydondan
// chiqqanda (har harfda serverga so'rov ketmasin)
mode: "onSubmit",
reValidateMode: "onBlur",
});MAYDONLAR— «Bitta forma, bir necha qadam» bo'limidagiFIELDSning o'zi.FieldPath<…>turi faqat haqiqiy maydon nomlariga ruxsat beradi —"tegg"deb yozilsa,tscxato beradi.fetchQuery({ staleTime: 0 })— Query keshida ro'yxat bor, lekin u 30 soniyagacha "yangi" hisoblanadi (kesh hayot sikli). Tekshiruv uchun hozirgi holat kerak —staleTime: 0serverga qayta so'raydi. Boshqa qurilmada hozirgina qo'shilgan vazifa ham ko'rinadi.mode: "onSubmit"+reValidateMode: "onBlur"— «So'rovni kamaytirish» bo'limidagi birinchi yo'l.
Diqqat qiling: resolver trigger(["teg"]) da ham butun sxemani ishlatadi (RHF + Zod), shu jumladan superRefine ni. Nega birinchi qadamda serverga so'rov ketmaydi? ctx.issues da faqat shu funksiya qo'shgan xatolar emas, oldingi qoidalarning xatolari ham bor. Birinchi qadamda qadam matni hali bo'sh (minLength(1) buzilgan) — ctx.issues.length > 0, funksiya return qiladi. Biz buni Node'da sinadik: teg va qadam bo'sh — ctx.issues 2 ta, so'rov yo'q; faqat qadam bo'sh — 1 ta, so'rov yo'q; hammasi to'g'ri — 0, so'rov ketdi.
"Keyingi" va fokus:
// Qadam almashdi — fokus yangi qadam sarlavhasiga (ekran o'quvchi
// qayerda ekanini eshitadi)
const birinchi = useRef(true);
useLayoutEffect(() => {
if (birinchi.current) {
birinchi.current = false;
return;
}
legendRef.current?.focus();
}, [qadam]);
async function keyingi() {
const maydonlar = MAYDONLAR[qadam] ?? [];
// trigger — faqat shu maydonlar; true — xato yo'q
if (await trigger([...maydonlar], { shouldFocus: true })) {
setQadam((q) => q + 1);
}
}useLayoutEffect — fokus ekran chizilishidan oldin qo'yiladi, sakrash ko'rinmaydi. birinchi — sahifa ochilganda fokus sarlavhaga emas, Qobiq qo'ygan joyda qolsin.
Qadamlar ro'yxati — useFieldArray (kanon):
// useFieldArray (19/#33): ro'yxat — massiv; har elementga
// barqaror id (key uchun — indeks emas: o'rtadagisi o'chsa, holat
// adashmaydi)
const { fields, append, remove } = useFieldArray({
control,
name: "qadamlar",
});Har qator — register(\qadamlar.${i}.matn`), o'chirish tugmasi aria-label={`O'chirish: ${i + 1}-qadam`}vadisabled={fields.length === 1}— oxirgi qatorni o'chirib bo'lmaydi. Massiv xatosi —errors.qadamlar?.root?.message ?? errors.qadamlar?.message`.
Yaratish — qadamlar ketma-ket POST (mutatsiyalar dagi useYozuv ichida), jarayon matni "Qo'shilmoqda: 2 / 3" (role="status"). Server id'larni tartib bilan beradi, shuning uchun parallel emas. Tugagach — e'lon va /?filtr=faol ga o'tish.
6.4 Marshrut va havola
marshrutlar.tsx va App.tsx (kanon):
+import { YangiLoyiha } from "./sahifalar/YangiLoyiha.tsx";
...
+ // 19/#33: ko'p qadamli forma (wizard)
+ {
+ path: "yangi",
+ element: <YangiLoyiha />,
+ handle: nomi("Qadamlarga bo'lib"),
+ },+ {/* 19/#33: katta ish — qadamlarga bo'lib (ko'p qadamli
+ forma) */}
+ <p className="izoh">
+ <Link to="/yangi" id="usta-havola">
+ Katta ishni qadamlarga bo'lib qo'shish…
+ </Link>
+ </p>6.5 Tekshiruvlar va commit
Yangi tekshiruv/loyiha-formasi.test.js — sxemaning 5 testi. Natijalar (haqiqiy, holat 33):
npm test— node:testℹ tests 187 · ℹ pass 187(182 + 5), VitestTests 64 passed (64). «Orqaga» tuzatishidan keyin sizda node:test — 195 (190 + 5).- Build:
index-….js 473.89 kB │ gzip: 150.83 kB— +3.3 KB (yangi sahifa; kutubxona yangi emas). - Brauzer: 159/159, 72/72, 19-qism — 85 (tuzatishlardan keyin 88). Wizard bo'limi:
✅ 1-qadam: teg noto'g'ri — xato, fokus maydonda, qadam o'zgarmadi: ["Teg # bilan boshlansin, bo'shliqsiz: #ta'mir.","usta-teg","Teg"]
✅ 2-qadam: fokus qadam sarlavhasida (legend): ["LEGEND","2/3: Qadamlar"]
✅ useFieldArray: qo'shildi — fokus yangi maydonda: "usta-qadam-1"
✅ async: serverda bor («Non olish #bozor») — o'sha qadamga xato; serverdan yangi ro'yxat so'raldi: ["«Non olish #bozor» ro'yxatda allaqachon bor.",["GET"]]
✅ 3-qadam: ko'rib chiqish: ["Sabzi olish #bozor"]
✅ yaratildi: POST, ro'yxat (faol), e'lon: [true,"/?filtr=faol","1 ta vazifa qo'shildi (#bozor).",true]
✅ wizard: CSP buzilishi yo'q: []To'rtinchi qator — async tekshiruvning isboti: so'rov bitta (["GET"]), xato aynan o'sha qadam ostida.
Bir "yon" topilma ham bo'ldi. Eski "qidiruv: filtr bilan birga" tekshiruvi (13–17-qismdan) holat 33 da uch ishga tushirishdan ikkitasida yiqildi. Sabab: filtr URL holati darsidan beri router navigatsiyasi — asinxron, ro'yxat bir zum eski edi. Tekshiruv endi URL va ro'yxat yangilanishini kutadi (kutilgan qiymat o'zgarmagan). Holat 32 da u tasodifan o'tib kelgan. Diff: 6 fayl, +482.
git add -A
git commitCommit (kanon): feat: /yangi — ko'p qadamli forma (useFieldArray, async tekshiruv).
useBlocker va avtosaqlash kanonda yo'q. Wizard qisqa (uch qadam), ma'lumot kichik, va 18-qismdan beri ro'yxat formasining o'z qoralamasi bor. Ularni 3-mashqda o'zingiz qo'shasiz.
7. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
| qator o'chirilgach boshqa qatorning holati "ko'chdi" | key={i} |
key={field.id} |
yozilgan qiymat fields[i] da yo'q |
fields — boshlang'ich qiymatlar |
useWatch / getValues |
| "Keyingi" hali ko'rinmagan maydonlarni ham xato deydi | trigger() argumentsiz |
trigger(["teg"]) |
| oxirgi qadamda oldingi qiymatlar yo'q | shouldUnregister: true |
sukutni qoldiring (false) |
| har harfda serverga so'rov | async validate + mode: "onChange" |
onBlur / debounce |
| eski javob yangisini bosib ketdi | parallel so'rovlar tartibsiz qaytdi | onBlur, yoki faqat oxirgi so'rov natijasini oling |
useBlocker must be used within a data router |
<BrowserRouter> |
createBrowserRouter + RouterProvider |
| sahifa qaytganda sekin ochiladi | beforeunload doim ulangan |
faqat isDirty bo'lganda ulang |
8. Mashqlar
Mashqlar kurs/mashqlar/19/33-dinamik/ da. Har yechimni «Ishga tushir» bilan sinang.
1-mashq (oson): qaysi biri?
- Massiv maydonida
keyuchun → - Wizard'da faqat joriy qadamni tekshiradigan metod →
- Ilova ichida chiqib ketishni to'xtatadigan React Router hook'i →
- Tab yopilayotganda ogohlantiradigan brauzer hodisasi →
- Async tekshiruvda har harfda so'rov ketmasligi uchun eng sodda
mode→
Yechim
field.id— RHF bergan barqaror kalit, qiymat bilan birga ko'chadi.trigger—trigger(["teg"])faqat shu maydonlarni tekshiradi,true/falseqaytaradi.useBlocker— faqat data router bilan;proceed()/reset().beforeunload— matnni brauzer o'zi tanlaydi.onBlur(yokionSubmit+reValidateMode: "onBlur").
2-mashq (o'rta): telefon raqamlari ro'yxati
Bron formasida mehmon bir nechta telefon qoldira olsin:
- Boshida bitta bo'sh qator; "Raqam qo'shish" — yangi qator; har qatorda "O'chirish: N-raqam" (oxirgisi o'chmaydi —
disabled). - Ko'pi bilan 3 ta raqam — "Raqam qo'shish" uchinchisidan keyin
disabled. - «Saqlash» — konsolga raqamlar massivi.
import { useForm } from "react-hook-form";
export default function App() {
const { register, handleSubmit } = useForm({
defaultValues: { phones: [{ number: "" }] },
});
return (
<form onSubmit={handleSubmit((d) => console.log(d.phones))}>
<input aria-label="1-raqam" {...register("phones.0.number")} />
<button>Saqlash</button>
</form>
);
}Konsolda:
(1) [{number: ''}]Yechim
import { useFieldArray, useForm } from "react-hook-form";
const MAX = 3;
export default function App() {
const { register, control, handleSubmit } = useForm({
defaultValues: { phones: [{ number: "" }] },
});
const { fields, append, remove } = useFieldArray({
control,
name: "phones",
});
return (
<form onSubmit={handleSubmit((d) => console.log(d.phones))}>
<ol>
{fields.map((field, i) => (
<li key={field.id}>
<input
aria-label={`${i + 1}-raqam`}
{...register(`phones.${i}.number`)}
/>
<button
type="button"
aria-label={`O'chirish: ${i + 1}-raqam`}
disabled={fields.length === 1}
onClick={() => remove(i)}
>
✕
</button>
</li>
))}
</ol>
<button
type="button"
disabled={fields.length >= MAX}
onClick={() => append({ number: "" })}
>
Raqam qo'shish
</button>
<button>Saqlash</button>
</form>
);
}Konsolda:
(3) [{number: '+998 90 000 00 00'}, {number: '+998 91 000 00 00'}, {number: ''}]Uchinchi qator qo'shildi va bo'sh qoldi — "Raqam qo'shish" endi o'chgan. Bo'sh raqamlarni yuborishdan oldin tashlash yoki xato berish — sxemaning ishi: z.array(z.object({ number: … })).min(1) va bo'shlarini transform bilan olib tashlash (Zod chuqurroq).
3-mashq (qiyin): taom tahriri — chiqishdan himoya
«Bahor» xodimi taom narxini tahrirlaydi:
- Ikki sahifa (xotiradagi router):
/— "Osh tahriri" havolasi,/osh— forma (price, boshlang'ich"35000"), "Menyuga" havolasi va «Saqlash». - O'zgarish bo'lsa va «Saqlash» bosilmagan bo'lsa — "Menyuga" bosilganda
useBlockeroynasi: "Saqlanmagan narx bor" + "Chiqish" / "Qolish". - «Saqlash» — konsolga
saqlandi: <narx>, keyinreset(data): endi chiqish so'ralmaydi.
Ishora: useBlocker(isDirty). reset dan keyin isDirty — false (RHF II 3-mashqi).
Yechim
import { useForm } from "react-hook-form";
import {
createMemoryRouter,
Link,
RouterProvider,
useBlocker,
} from "react-router";
function EditDish() {
const {
register,
handleSubmit,
reset,
formState: { isDirty },
} = useForm({ defaultValues: { price: "35000" } });
const blocker = useBlocker(isDirty);
function save(data) {
console.log("saqlandi:", data.price);
reset(data);
}
return (
<form onSubmit={handleSubmit(save)}>
<p>
<label htmlFor="price">Narxi</label>
<input id="price" {...register("price")} />
</p>
<button>Saqlash</button> <Link to="/">Menyuga</Link>
{blocker.state === "blocked" && (
<div role="alertdialog" aria-label="Chiqishni tasdiqlang">
<p>Saqlanmagan narx bor</p>
<button type="button" onClick={() => blocker.proceed()}>
Chiqish
</button>
<button type="button" onClick={() => blocker.reset()}>
Qolish
</button>
</div>
)}
</form>
);
}
const router = createMemoryRouter([
{ path: "/", element: <Link to="/osh">Osh tahriri</Link> },
{ path: "/osh", element: <EditDish /> },
]);
export default function App() {
return <RouterProvider router={router} />;
}Konsolda:
saqlandi: 37000Ekranda:
Osh tahririButun yo'l: narx o'zgardi → "Menyuga" → oyna → "Qolish" (forma joyida) → «Saqlash» → reset → "Menyuga" — endi so'ramasdan menyuga o'tdi.
4-mashq: Vazifalar qadami
feat/qadamlarga-bolish branch'ida sxemalar/loyiha-formasi.ts (testi bilan), sahifalar/YangiLoyiha.tsx, marshrut va ro'yxatdagi havolani qo'shing. Commit.
Yechim
Tekshiruv ro'yxati:
npm test— node:test 195 («Orqaga» tuzatishigacha — 187), Vitest 64.npm run dev→/yangi: tegta'mir(#siz) → "Keyingi" — xato, fokus maydonda.#ta'mir→ 2-qadam, fokus "2/3: Qadamlar" sarlavhasida.- "Qadam qo'shish" — fokus yangi maydonda. Mavjud vazifa matnini qadam qilib yozing ("Non olish", teg
#bozor) → "Keyingi" — xato o'sha qator ostida; DevTools Network'da bitta GET. - "Yaratish" — POST'lar ketma-ket, keyin ro'yxat
?filtr=faolbilan, e'lon "N ta vazifa qo'shildi (#teg)". git log --oneline -1—feat: /yangi — ko'p qadamli forma (useFieldArray, async tekshiruv).
9. Real ishda
- Wizard'lar — ro'yxatdan o'tish, kredit arizasi, onlayn-do'kon checkout'i (manzil → yetkazish → to'lov). Ko'pincha har qadam URL'da ham (
/checkout/manzil) — orqaga tugmasi va sahifa yangilanishi uchun (URL — holat joyi). - Dinamik maydonlar — hisob-faktura qatorlari, so'rovnoma savollari, oila a'zolari. Katta ro'yxatda (100+ qator) har qatorni alohida komponentga va
useWatchga ajratish muhim. - Async tekshiruv — login/email bandligi, promokod, INN. Har doim serverda qayta tekshiriladi; brauzerdagi — qulaylik.
- Intervyu savollari: "
useFieldArrayda negafield.id?", "ko'p qadamli formani qanday qurasiz?", "async validatsiyada so'rovlar sonini qanday kamaytirasiz?", "foydalanuvchi saqlamay chiqib ketsa nima qilasiz?".
Xulosa
useFieldArray—fields(key={field.id}),append,remove,move; nomlaritems.${i}.dish; joriy qiymat —useWatch.- Wizard — bitta
useForm, har qadamdatrigger([...]); qiymatlar unmount'dan keyin ham qoladi; qadam almashganda fokus sarlavhaga. - Async tekshiruv —
validateyokisuperRefine; so'rov —onBlur/debounce bilan kam, avval arzon qoidalar. Kafolat — server. - Saqlanmagan o'zgarish: ilova ichida —
useBlocker(isDirty), tab yopilganda —beforeunload(faqatisDirtyda); eng yaxshisi — avtosaqlash.
Keyingi dars: Validatsiya va forma kutubxonalari muqobillari — Formik, TanStack Form, Conform, Valibot va Standard Schema: bitta formani besh xil yozib, hajmini o'lchaymiz.
Manbalar
- React Hook Form:
useFieldArray,trigger,register(validate),formState.isValidating— react-hook-form.com/docs (2026-10 holatiga, v7.89.0) - React Router:
useBlocker,useBeforeUnload— reactrouter.com/api/hooks/useBlocker (v8.4.0) - MDN:
beforeunloadhodisasi va bfcache — developer.mozilla.org/docs/Web/API/Window/beforeunload_event - WAI-ARIA:
aria-current="step"— w3.org/TR/wai-aria-1.2/#aria-current
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!