Mundarija (29)
- Bu darsda
- 1. Nega bu kerak?
- 2. Controller — maxsus komponentni ulash
- 2.1 Muammo: register va tugma
- 2.2 Controller bilan
- 2.3 fieldState — maydonning o'z holati
- 2.4 useController — hook ko'rinishi
- 3. Qiymatlarni kuzatish va boshqarish
- 3.1 watch va useWatch
- 3.2 setValue va getValues
- 4. Tahrirlash formasi: reset va dirtyFields
- 4.1 Ma'lumot keyin keladi
- 4.2 values prop va boshqa yo'llar
- 5. useFormContext — katta forma bo'laklarda
- 6. Vazifalar qadami: tahrir formasi tugadi (holat 32)
- 6.1 Almashtirgich: ui/Almashtirgich.tsx
- 6.2 Jonli teglar: useWatch
- 6.3 useOrqaga hook'i
- 6.4 e2e topgan xato: ota loader qayta ishlamaydi
- 6.5 Testlar, tekshiruvlar va commit
- 7. Ko'p uchraydigan xatolar
- 8. Mashqlar
- 1-mashq (oson): qaysi vosita?
- 2-mashq (o'rta): achchiqlik darajasi
- 3-mashq (qiyin): taom tahriri — faqat o'zgarganini yubor
- 4-mashq: Vazifalar qadami
- 9. Real ishda
- Xulosa
- Manbalar
React Hook Form Controller, useWatch va tahrirlash formasi: reset, dirtyFields, useFormContext
Qisqacha:
registerfaqat haqiqiy<input>bilan ishlaydi. O'z komponentingiz (switch, combobox, UI kutubxona inputi) —ControlleryokiuseControllerbilan ulanadi: RHF ungavalue,onChange,onBlur,refberadi. Qiymatni kuzatish —useWatch(faqat o'sha komponent qayta chiziladi),watch(butun forma).setValue— qiymat qo'yish,getValues— o'qish (render'siz). Serverdan kelgan ma'lumot —reset(data);dirtyFields— faqat o'zgargan maydonlar. Ichma-ich komponentlar formaniuseFormContextbilan oladi.
Bu darsda
- Maxsus komponentni
ControllervauseControllerbilan formaga ulaysiz,fieldvafieldStateni ishlatasiz. watchvauseWatchning render farqini o'lchaysiz,setValuevagetValuesni qachon ishlatishni bilasiz.- Serverdan kelgan ma'lumotni
resetbilan formaga qo'yasiz va faqatdirtyFieldsni yuborasiz. - Katta formani bo'laklarga
FormProvidervauseFormContextbilan ajratasiz. vazifalartahrir formasini tugatasiz: almashtirgich, jonli teglar,useOrqaga, testlar va commit.
Oldin bilishingiz kerak: RHF + Zod: resolver, Controlled va uncontrolled komponent API dizayni, useRef II: ref prop, React Router III.
1. Nega bu kerak?
RHF I da RHF ning siri ochildi: register qiymatni ref orqali DOM'dan o'qiydi. Bu oddiy <input>, <select>, <textarea> da ishlaydi. Endi Jasur aka tahrir formasida uch narsani so'radi:
- "Bajarildi" — almashtirgich (switch) bo'lsin, katak emas. Sardor uni
<button role="switch">bilan yasadi. Tugmadavalueyo'q —registeruni o'qiy olmaydi. - Matn yozilayotganda teglar jonli ko'rinsin: "Teglar: #bozor". Lekin RHF harf yozilganda qayta chizmaydi — qanday ko'rsatamiz?
- Forma serverdan kelgan vazifa bilan to'lsin va "Saqlash" faqat o'zgargan maydonlarni yuborsin.
Hayotiy o'xshatish: universal pult. Televizor (<input>) pultni o'zi tushunadi — register. Konditsioner (o'z komponentingiz) esa boshqa tilda gapiradi. Controller — pult va konditsioner orasidagi moslagich: "yoqildi" buyrug'ini konditsioner tushunadigan tilga o'giradi va uning holatini pultga qaytaradi.
2. Controller — maxsus komponentni ulash
2.1 Muammo: register va tugma
Avval xatoni ko'ramiz. Almashtirgich — <button role="switch">. register ni unga yoyamiz:
import { useForm } from "react-hook-form";
export default function App() {
const { register, handleSubmit } = useForm({
defaultValues: { done: false },
});
return (
<form onSubmit={handleSubmit((d) => console.log(d))}>
<button type="button" role="switch" {...register("done")}>
Bajarildi
</button>
<button>Saqlash</button>
</form>
);
}Konsolda:
{done: false}Tugma bosildi, lekin natija — false, boshlang'ich qiymat. RHF bosishni umuman sezmadi. register maydonning change hodisasini tinglaydi va qiymatni elementning value yoki checked idan o'qiydi. Tugmada esa na change hodisasi bor, na checked. Xato ham, ogohlantirish ham chiqmadi — shuning uchun bunday xatoni topish qiyin.
2.2 Controller bilan
Controller — komponent. U control (formaning "boshqaruv pulti", useForm dan) va maydon name ini oladi. render funksiyasiga field beradi: value, onChange, onBlur, ref, name. Maydon endi controlled — qiymat RHF da, komponent uni props orqali oladi (Controlled va uncontrolled):
import { Controller, useForm } from "react-hook-form";
function Switch({ checked, onChange, onBlur, ref, children }) {
return (
<button
ref={ref}
type="button"
role="switch"
aria-checked={checked}
onClick={() => onChange(!checked)}
onBlur={onBlur}
>
{checked ? "● " : "○ "}
{children}
</button>
);
}
export default function App() {
const { control, handleSubmit } = useForm({
defaultValues: { done: false },
});
return (
<form onSubmit={handleSubmit((d) => console.log(d))}>
<Controller
control={control}
name="done"
render={({ field }) => (
<Switch
ref={field.ref}
checked={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
>
Bajarildi
</Switch>
)}
/>
<p>
<button>Saqlash</button>
</p>
</form>
);
}Konsolda:
{done: true}Ekranda:
● Bajarildi
SaqlashTo'rt ulanish — to'rtta prop:
field |
Komponentda | Nega |
|---|---|---|
value |
checked |
joriy qiymat (controlled) |
onChange |
yangi qiymat bilan chaqiriladi | RHF qiymatni yangilaydi va (mode bo'yicha) tekshiradi |
onBlur |
fokus ketganda | touched — onTouched rejimi shunga tayanadi |
ref |
DOM elementga | xato bo'lsa fokus shu yerga (RHF I) |
field.onChange ga hodisa ham, qiymat ham berish mumkin. onChange(event) bo'lsa, RHF event.target.value (yoki checked) ni oladi. onChange(true) bo'lsa — qiymatning o'zini. Maxsus komponentda odatda qiymat beriladi.
ref — React 19 da oddiy prop (ref prop): Switch uni <button> ga o'tkazadi. Usiz forma ishlaydi, lekin xato bo'lganda fokus bu maydonga o'tmaydi.
2.3 fieldState — maydonning o'z holati
render ikkinchi qism ham oladi — fieldState: invalid, error, isTouched, isDirty. Bu — formState ning shu maydonga tegishli bo'lagi. Shartlar roziligi uchun almashtirgich, sxema z.literal(true) (Zod chuqurroq):
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
const Schema = z.object({
agree: z.literal(true, { error: "Qoidalarga rozilik kerak" }),
});
export default function App() {
const { control, handleSubmit } = useForm({
resolver: zodResolver(Schema),
defaultValues: { agree: false },
});
return (
<form noValidate onSubmit={handleSubmit((d) => console.log(d))}>
<Controller
control={control}
name="agree"
render={({ field, fieldState }) => (
<p>
<button
ref={field.ref}
type="button"
role="switch"
aria-checked={field.value}
aria-invalid={fieldState.invalid || undefined}
onClick={() => field.onChange(!field.value)}
onBlur={field.onBlur}
>
Qoidalarga roziman
</button>
<span role="alert"> {fieldState.error?.message}</span>
</p>
)}
/>
<button>Yuborish</button>
</form>
);
}Ekranda:
Qoidalarga roziman Qoidalarga rozilik kerak
YuborishXato almashtirgich yonida — fieldState.error dan. errors.agree ham xuddi shu obyekt; fieldState faqat "shu maydonniki"ni olishni qulay qiladi.
2.4 useController — hook ko'rinishi
Controller ichida render prop bor (render props). Ko'p ishlatiladigan komponentda buni har safar yozmaslik uchun useController hook'i bor. U o'sha field va fieldState ni qaytaradi:
import { useController, useForm } from "react-hook-form";
function PortionPicker({ control, name }) {
const { field } = useController({ control, name });
return (
<fieldset>
<legend>Porsiya</legend>
{["Kichik", "O'rta", "Katta"].map((size) => (
<button
key={size}
type="button"
aria-pressed={field.value === size}
onClick={() => field.onChange(size)}
>
{size}
</button>
))}
</fieldset>
);
}
export default function App() {
const { control, handleSubmit } = useForm({
defaultValues: { portion: "O'rta" },
});
return (
<form onSubmit={handleSubmit((d) => console.log(d))}>
<PortionPicker control={control} name="portion" />
<button>Saqlash</button>
</form>
);
}Konsolda:
{portion: 'Kichik'}PortionPicker endi istalgan formada bir qator bilan ishlatiladi: <PortionPicker control={control} name="…" />. UI kutubxonalar (MUI, Ant Design, shadcn) bilan ishlashning odatiy yo'li ham shu: har kutubxona inputiga bitta useController li o'rovchi (UI kutubxonalar xaritasi).
Tekshirib ko'ring:
<input>ni hamControllerbilan ulash mumkinmi? Kerakmi?
Javob
Mumkin: render={({ field }) => <input {...field} />}. Lekin unda maydon controlled bo'ladi — har harfda Controller qayta chiziladi. Oddiy <input> uchun register yaxshiroq: render yo'q. Controller — register ishlay olmaydigan joylar uchun: value si yo'q yoki onChange ga hodisa emas, qiymat beradigan komponentlar.
3. Qiymatlarni kuzatish va boshqarish
3.1 watch va useWatch
Teglar jonli ko'rinishi uchun matnni "kuzatish" kerak. Ikki yo'l bor. watch("name") — useForm qaytaradigan funksiya: u formaning o'zini (shu komponentni) har o'zgarishda qayta chizadi. useWatch({ control, name }) — hook: u faqat o'zi turgan komponentni qayta chizadi.
O'lchaymiz. Ikkala forma bir xil, faqat kuzatish joyi boshqa. Mehmon uch harf yozdi:
import { useForm } from "react-hook-form";
let formRenders = 0;
export default function App() {
const { register, watch } = useForm({
defaultValues: { text: "" },
});
formRenders += 1;
const text = watch("text");
return (
<form>
<p>
<label htmlFor="text">Matn</label>
<input id="text" {...register("text")} />
</p>
<p>Ko'rinish: {text}</p>
<p>Forma chizildi: {formRenders}</p>
</form>
);
}Ekranda:
Matn
Ko'rinish: Osh
Forma chizildi: 5import { useForm, useWatch } from "react-hook-form";
let formRenders = 0;
function Preview({ control }) {
const text = useWatch({ control, name: "text" });
return <p>Ko'rinish: {text}</p>;
}
export default function App() {
const { register, control } = useForm({
defaultValues: { text: "" },
});
formRenders += 1;
return (
<form>
<p>
<label htmlFor="text">Matn</label>
<input id="text" {...register("text")} />
</p>
<Preview control={control} />
<p>Forma chizildi: {formRenders}</p>
</form>
);
}Ekranda:
Matn
Ko'rinish: Osh
Forma chizildi: 2Natija bir xil — "Ko'rinish: Osh". Lekin watch bilan forma 5 marta chizildi (2 ta boshlang'ich — RHF I dagi kabi — va 3 harf). useWatch bilan — 2: harflar faqat Preview ni qayta chizdi. 30 maydonli formada bu farq sezilarli. Qoida: kuzatishni kerak joyga — kichik komponentga — qo'ying.
watch |
useWatch |
|
|---|---|---|
| Qayta chizadi | useForm chaqirilgan komponentni |
o'zi turgan komponentni |
| Qayerda | formaning o'zida | istalgan bola komponentda (control bilan) |
| Qachon qulay | kichik forma, tez yozish | katta forma, ko'rinish alohida |
watch() argumentsiz — butun forma qiymati. Hodisa sifatida kuzatish ham bor: watch((values, { name }) => …) yoki RHF 7.55 dan beri subscribe({ formState: { values: true }, callback }) — qayta chizmasdan, faqat funksiya chaqiradi (masalan, qoralamani saqlash — RHF III).
3.2 setValue va getValues
Kodning o'zi qiymat qo'ymoqchi bo'lsa — setValue(name, value, options). Masalan, "tez tanlov" tugmalari. getValues() — joriy qiymatlarni qayta chizmasdan o'qiydi:
import { useForm } from "react-hook-form";
export default function App() {
const {
register,
setValue,
getValues,
formState: { isDirty },
} = useForm({ defaultValues: { dish: "" } });
return (
<form>
<p>
<label htmlFor="dish">Taom</label>
<input id="dish" {...register("dish")} />
</p>
{["Osh", "Lag'mon"].map((d) => (
<span key={d}>
<button
type="button"
onClick={() => setValue("dish", d, { shouldDirty: true })}
>
{d}
</button>{" "}
</span>
))}
<button type="button" onClick={() => console.log(getValues())}>
Tekshir
</button>
<p>isDirty: {String(isDirty)}</p>
</form>
);
}Konsolda:
{dish: "Lag'mon"}Ekranda:
Taom
Osh Lag'mon Tekshir
isDirty: truesetValue ning uchinchi argumenti muhim. Sukut bo'yicha setValue qiymatni jim qo'yadi: isDirty o'zgarmaydi, tekshiruv ishlamaydi. Kerak bo'lsa aytasiz:
| Sozlama | Nima qiladi |
|---|---|
shouldDirty: true |
isDirty / dirtyFields yangilanadi |
shouldValidate: true |
shu maydon darhol tekshiriladi |
shouldTouch: true |
maydon "tegilgan" bo'ladi |
getValues — hodisa ichida o'qish uchun (tugma bosildi — joriy qiymat kerak). Render paytida ko'rsatish uchun esa useWatch: getValues qayta chizishni chaqirmaydi, ekran eskirib qoladi.
Tekshirib ko'ring:
setValue("dish", "Osh")(sozlamasiz) dan keyin forma yuborildi.onSubmitdagidata.dishnima?isDirtynima?
Javob
data.dish — "Osh": qiymat qo'yilgan, handleSubmit uni yig'adi. isDirty esa false bo'lib qoladi — shouldDirty berilmagan. "Saqlash faqat o'zgarganda" mantig'i bunday holatda xato ishlaydi. Koddan qo'yilgan qiymat foydalanuvchi o'zgarishi hisoblanishi kerak bo'lsa — { shouldDirty: true }.
4. Tahrirlash formasi: reset va dirtyFields
4.1 Ma'lumot keyin keladi
Tahrirlash formasi bo'sh emas — u mavjud yozuv bilan ochiladi. Ma'lumot serverdan keyin keladi, useForm esa birinchi chizishda chaqiriladi. defaultValues faqat birinchi marta o'qiladi — keyin uni o'zgartirish formani yangilamaydi.
Yechim — reset(data): forma qiymatlari va boshlang'ich qiymatlari almashadi. Shundan keyin isDirty yangi ma'lumotga nisbatan hisoblanadi. Soxta server 200 ms da taomni qaytaradi:
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
function fetchDish() {
return new Promise((resolve) =>
setTimeout(() => resolve({ name: "Osh", price: "35000" }), 200),
);
}
export default function App() {
const [loading, setLoading] = useState(true);
const {
register,
reset,
handleSubmit,
formState: { isDirty, dirtyFields },
} = useForm({ defaultValues: { name: "", price: "" } });
useEffect(() => {
fetchDish().then((dish) => {
reset(dish);
setLoading(false);
});
}, [reset]);
function save(data) {
const changed = Object.fromEntries(
Object.keys(dirtyFields).map((key) => [key, data[key]]),
);
console.log("PATCH", changed);
}
if (loading) return <p>Yuklanmoqda…</p>;
return (
<form onSubmit={handleSubmit(save)}>
<p>
<label htmlFor="name">Nomi</label>
<input id="name" {...register("name")} />
</p>
<p>
<label htmlFor="price">Narxi</label>
<input id="price" {...register("price")} />
</p>
<p>O'zgargan: {isDirty ? "ha" : "yo'q"}</p>
<button>Saqlash</button>
</form>
);
}Konsolda:
PATCH {price: '37000'}Ekranda:
Nomi
Narxi
O'zgargan: ha
SaqlashQadamma-qadam:
| Qadam | Maydonlarda | isDirty / dirtyFields |
|---|---|---|
| sahifa ochildi | (forma yo'q) — "Yuklanmoqda…" | — |
200 ms — reset(dish) |
Osh, 35000 | false / {} |
| narx 37000 ga o'zgardi | Osh, 37000 | true / { price: true } |
| «Saqlash» | — | PATCH {price: '37000'} |
reset boshlang'ich qiymatni ham almashtirgani uchun ikkinchi qatorda isDirty — false. Narx o'zgardi — serverga faqat price ketdi. dirtyFields — { price: true }: qaysi maydonlar boshlang'ichdan farq qiladi. Nom tegilmagan — yuborilmaydi. Ikki kishi bir vaqtda bitta taomni tahrirlasa (biri nomini, biri narxini), ular bir-birining o'zgarishini bosib ketmaydi.
reset ni effektda chaqirdik — bu tashqi tizim (server) bilan sinxronlash (useEffect I). Real loyihada ma'lumot Query'dan keladi va odatda useEffect yozilmaydi — keyingi bo'limdagi values yoki loader.
4.2 values prop va boshqa yo'llar
reset dan tashqari yana uch yo'l bor:
| Yo'l | Qanday | Qachon |
|---|---|---|
reset(data) effektda |
yuqoridagidek | ma'lumot keyin keladi, nazorat sizda |
values: data |
useForm({ values: data }) |
Query kabi manba — data o'zgarsa, forma yangilanadi |
defaultValues: async () => … |
Promise qaytaradigan funksiya | bir martalik yuklash, isLoading bilan |
| ma'lumot oldin keladi | loader (data mode), keyin defaultValues |
router'li ilova — vazifalar |
values ga ehtiyot bo'ling: manba yangilansa (masalan, Query fonda qayta so'radi), forma ham yangilanadi — mehmon yozib turgan matn bosilib ketishi mumkin. Buning uchun resetOptions: { keepDirtyValues: true } — o'zgartirilgan maydonlar saqlanadi, qolganlari yangilanadi.
vazifalar da esa bularning hech biri kerak emas: vazifani loader sahifa chizilishidan oldin oladi, forma ochilganda ma'lumot tayyor. Oddiy defaultValues yetadi — eng sodda yo'l.
5. useFormContext — katta forma bo'laklarda
Forma o'sdi: manzil bo'limi, to'lov bo'limi, izoh. Har bo'lim — alohida komponent. register va control ni har biriga props bilan uzatish — prop drilling. RHF ning o'z Context'i bor: FormProvider formani ichiga qo'yadi, useFormContext() uni istalgan bolada oladi:
import {
FormProvider,
useForm,
useFormContext,
} from "react-hook-form";
function AddressSection() {
const { register } = useFormContext();
return (
<fieldset>
<legend>Manzil</legend>
<label htmlFor="street">Ko'cha</label>
<input id="street" {...register("address.street")} />
</fieldset>
);
}
function PaymentSection() {
const { register } = useFormContext();
return (
<fieldset>
<legend>To'lov</legend>
<select aria-label="To'lov usuli" {...register("payment")}>
<option value="naqd">Naqd</option>
<option value="karta">Karta</option>
</select>
</fieldset>
);
}
export default function App() {
const form = useForm({
defaultValues: { address: { street: "" }, payment: "naqd" },
});
return (
<FormProvider {...form}>
<form onSubmit={form.handleSubmit((d) => console.log(d))}>
<AddressSection />
<PaymentSection />
<button>Buyurtma berish</button>
</form>
</FormProvider>
);
}Konsolda:
{address: {street: 'Navoiy 12'}, payment: 'naqd'}{...form} — useForm qaytargan hamma narsa Context'ga. "address.street" — nuqtali nom ichma-ich obyekt yasadi (RHF I dagi maslahat). Bo'limlar endi har qanday formada ishlaydi — faqat FormProvider ichida bo'lsin. Context'dan tashqarida useFormContext() null qaytaradi va register ni olishda xato beradi.
useFormContext — qulaylik, tezlik emas. Kuzatish uchun baribir useWatch ishlating. Kichik formada (3–5 maydon) props bilan uzatish ko'pincha soddaroq.
flowchart TD
U["useForm()"] --> P["FormProvider"]
P --> A["AddressSection<br/>useFormContext"]
P --> B["PaymentSection<br/>useFormContext"]
P --> C["Preview<br/>useWatch"]6. Vazifalar qadami: tahrir formasi tugadi (holat 32)
6.1 Almashtirgich: ui/Almashtirgich.tsx
"Bajarildi" — role="switch" li tugma (kanon, to'liq; izoh qatorlari telefonga sig'ishi uchun bo'lingan):
// Almashtirgich.tsx — yoqish/o'chirish tugmasi (19/#32):
// role="switch" (ARIA), aria-checked — holat. Native <input>
// emas: React Hook Form uni register bilan ULAY OLMAYDI (ref'da
// value/checked yo'q) — Controller orqali boshqariladi (value +
// onChange + onBlur)
import type { ReactNode, Ref } from "react";
type Props = {
id: string;
yoqilgan: boolean;
onOzgar: (yoqilgan: boolean) => void;
onBlur?: () => void;
ref?: Ref<HTMLButtonElement>;
children: ReactNode; // nomi (ekran o'quvchi shuni o'qiydi)
};
export function Almashtirgich({
id,
yoqilgan,
onOzgar,
onBlur,
ref,
children,
}: Props) {
return (
<button
ref={ref}
type="button"
role="switch"
id={id}
className="almashtirgich"
aria-checked={yoqilgan}
onClick={() => onOzgar(!yoqilgan)}
onBlur={onBlur}
>
{children}
</button>
);
}«Controller bilan» bo'limidagi Switch ning o'zi, faqat turlar va o'zbekcha nomlar bilan. Komponent RHF haqida hech narsa bilmaydi — u oddiy controlled komponent (yoqilgan + onOzgar). Uni formasiz ham ishlatish mumkin. Ulanish — formada (kanon):
{/* Controller (19/#32): RHF ref ulay olmaydigan element —
qiymat va o'zgarish props orqali (boshqariladigan) */}
<Controller
control={control}
name="bajarildi"
render={({ field }) => (
<Almashtirgich
id="tahrir-bajarildi"
ref={field.ref}
yoqilgan={field.value}
onOzgar={field.onChange}
onBlur={field.onBlur}
>
Bajarildi
</Almashtirgich>
)}
/>Ko'rinish — CSS'da aria-checked bo'yicha: ○ / ● belgisi va qalin matn (vazifalar.css, 22 qator). Holatni rang bilan emas, belgi va atribut bilan ko'rsatish — accessibility asoslari qoidasi.
6.2 Jonli teglar: useWatch
Kanon:
// useWatch (19/#32): faqat shu qiymat o'zgarganda — teglar
// ko'rinishi
const matn = useWatch({ control, name: "matn" });
const teglar = [...teglariniOl(matn)];JSX'da:
<p id="tahrir-teglar">
{teglar.length === 0
? "Teg yo'q"
: `Teglar: ${teglar.join(", ")}`}
</p>teglariniOl — ro'yxatdagi teglarni ajratadigan funksiya (Suspense darsidan). Bu yerda useWatch formaning o'zida chaqirilgan, ya'ni VazifaTahriri har harfda qayta chiziladi — «watch va useWatch» bo'limidagi birinchi holat. Forma kichik (ikki maydon), shuning uchun alohida komponentga chiqarilmagan. Katta formada Teglar alohida komponent bo'lardi.
6.3 useOrqaga hook'i
«Bekor qilish» va saqlangandan keyingi qaytish — React Router III dagi «Orqaga» mantig'i. Endi u ikki joyda kerak (vazifa sahifasi va tahrir), shuning uchun alohida hook'ga chiqarildi (kanon, «Orqaga» tuzatishidan keyingi varianti):
// useOrqaga.ts — «Orqaga» (19/#04, #32 da hook'ga chiqarildi):
// ilova ichidan kelingan bo'lsa — tarixda bir qadam, havola bilan
// to'g'ridan ochilgan bo'lsa — zaxira manzilga (replace). Qadam —
// ilova ichidagi tarix chuqurligi (tarix-chuqurligi.ts): 0 — orqada
// ilovaning yozuvi yo'q
import { useLocation, useNavigate } from "react-router";
import { joriyQadam } from "../tarix-chuqurligi.ts";
export function useOrqaga(zaxira: string): () => void {
const navigate = useNavigate();
const { key } = useLocation();
return () => {
if (joriyQadam(key) === 0) {
void navigate(zaxira, { replace: true });
} else {
void navigate(-1);
}
};
}joriyQadam(key) — #04 dagi tarix-chuqurligi.ts dan: ilova ichida orqada nechta yozuv bor. location.key === "default" ga tayanilmaydi — u replace dan keyin ishonchsiz ekanini «Chekka holat» bo'limida ko'rgansiz. Yangi narsa ikkita:
zaxira— parametr. Vazifa sahifasidan —"/"(ro'yxat), tahrirdan —/vazifa/${vazifa.id}.{ replace: true }— zaxiraga o'tishda tarixga yangi yozuv qo'shilmaydi. Mehmon havola bilan/vazifa/2/tahrirni ochib, «Bekor qilish» ni bossa,/vazifa/2ga o'tadi. Keyin brauzerning «Orqaga» tugmasi uni yana tahrirga qaytarmasligi kerak —replaceshuni ta'minlaydi.
VazifaSahifasi.tsx dagi OrqagaTugmasi ham endi shu hook'ni ishlatadi (kanon diff):
function OrqagaTugmasi() {
- const navigate = useNavigate();
- const { key } = useLocation();
+ const orqaga = useOrqaga("/");
return (
- <button
- type="button"
- id="orqaga"
- onClick={() => {
- if (joriyQadam(key) === 0) {
- void navigate("/");
- } else {
- void navigate(-1);
- }
- }}
- >
+ <button type="button" id="orqaga" onClick={orqaga}>
Orqaga
</button>
);Kichik farq: zaxiraga o'tish endi replace bilan (oldin oddiy navigate("/")). Ro'yxatdan «Orqaga» bosilib yana vazifaga qaytish — mantiqsiz holat edi.
6.4 e2e topgan xato: ota loader qayta ishlamaydi
Vazifa sahifasiga ikki bo'lim qo'shildi — «Umumiy» va «Tahrirlash» (NavLink, React Router II). Keyin brauzer tekshiruvi (e2e) xato topdi: tahrirda matnni o'zgartirib saqlaysiz, /vazifa/1 ga qaytasiz — sarlavhada eski matn.
Sabab: /vazifa/1/tahrir → /vazifa/1 da ota marshrut (vazifa/:id) joyida qoladi. Parametr o'zgarmagan, shuning uchun router uning loader'ini qayta chaqirmaydi — loader ma'lumoti saqlashdan oldingi bo'lib qoldi. Tuzatish (kanon diff):
- const vazifa = useLoaderData<typeof vazifaLoader>();
+ const yuklangan = useLoaderData<typeof vazifaLoader>();
+ // 19/#32: ekrandagi qiymat — Query keshidan (jonli),
+ // loader'niki — faqat zaxira. Sabab (e2e topdi): /vazifa/1/tahrir dan Orqaga'da
+ // ota marshrut ("vazifa/:id") joyida qoladi va router uning
+ // loader'ini QAYTA chaqirmaydi (parametr o'zgarmagan) — loader
+ // ma'lumoti saqlashdan oldingi bo'lib qolardi. Loader — "bormi,
+ // ochsa bo'ladimi" (404, kirish); kesh — "hozir qanday"
+ const { vazifalar } = useVazifalar();
+ const vazifa =
+ vazifalar.find((v) => v.id === yuklangan.id) ?? yuklangan;Ikki manbaning vazifasi bo'lindi: loader — "bu vazifa bormi, sahifani ochsa bo'ladimi" (404, kirish); Query keshi — "hozir qanday". Saqlash keshni yangilaydi (mutatsiyalar), sahifa esa keshdan o'qiydi. Router o'z loader'ini qachon qayta chaqirishini boshqarish ham mumkin (shouldRevalidate), lekin kesh allaqachon bor edi — ikkinchi haqiqat manbai kerak emas.
Bu tuzatishning kutilmagan oqibati — Vitest. Birinchi npm test da VazifaSahifasi.test.tsx ning 5 testi yiqildi (natijalar/tekshiruv-32.txt): sahifa endi useVazifalar() ni chaqiradi, testda esa QueryClientProvider yo'q edi. Tuzatish — testning wrapper ida provider:
- render(<RouterProvider router={router} />);
+ // 19/#32: sahifa vazifani Query keshidan o'qiydi — Provider
+ // kerak
+ render(
+ <QueryClientProvider client={sorovMijozi}>
+ <RouterProvider router={router} />
+ </QueryClientProvider>,
+ );6.5 Testlar, tekshiruvlar va commit
Yangi Vitest fayli — tekshiruv/VazifaTahriri.test.tsx. Unda 5 holat: bo'sh matn (xato, aria-invalid, fokus, so'rov yo'q), takror, kesilgan matn + almashtirgich (PATCH), o'zgarishsiz saqlash (so'rov yo'q), server rad etdi (RHF + Zod darsida ko'rgansiz). Uchinchisining tekshiruvi (kanon):
expect(yozuvlar).toEqual([
'PATCH {"matn":"Non va sut olish #bozor","bajarildi":true}',
]);Serverga borgan tana — kesilgan matn (" Non va sut olish #bozor " yozilgan edi) va Controller dan kelgan true.
Tekshiruvlar (haqiqiy, holat 32):
npm test— node:testℹ tests 182 · ℹ pass 182, VitestTests 64 passed (64)(59 + 5). Bu xom natija «Orqaga» tuzatishidan oldin olingan; tuzatish har holatga 8 ta node testi qo'shgan, shuning uchun sizda — node:test 190.lint,format:check,tip,knip—exit 0.- Build:
index-….js 463.17 kB │ gzip: 147.55 kB— holat 22 dan +14.8 KB (RHF I dagi «Narxi»). - Brauzer: 159/159, 72/72, 19-qism tekshiruvlari 78 (keyinroq «Orqaga» va
/kirishtuzatishlari bilan 81). Tahrir formasiga tegishlilari:
✅ tahrir: boshlang'ich qiymatlar, non yo'li: ["Non olish #bozor","false",["Ro'yxat","Non olish #bozor","Tahrirlash"]]
✅ bo'sh — maydondan chiqqanda xato (onTouched), aria-describedby: ["Vazifa matnini yozing."]
✅ takror (boshqa vazifada bor) — xato, serverga yo'q, fokus maydonda: ["Bu vazifa ro'yxatda allaqachon bor.",[],"tahrir-matn"]
✅ useWatch: teglar jonli: "Teglar: #bozor"
✅ saqlandi: PATCH (kesilgan matn, Controller qiymati), sahifa yangi, e'lon: [{"id":1,"matn":"Non va sut olish #bozor","bajarildi":true},"Non va sut olish #bozor","O'zgarish saqlandi."]
✅ tahrir formasi: CSP buzilishi yo'q: []Beshinchi qatorga qarang: "sahifa yangi" — sarlavhada yangi matn. Bu — «ota loader» tuzatishining isboti. Diff: 12 fayl, +704 −67.
git add -A
git commitCommit (kanon): feat: /vazifa/:id/tahrir — React Hook Form + Zod, almashtirgich (Controller).
To'rt dars davomida yig'ilgan forma tayyor: register va xatolar (RHF I), sxema (Zod), resolver va server xatosi (RHF + Zod), Controller va useWatch (shu dars).
7. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
maxsus komponent qiymati '' yoki undefined |
register yoyilgan, komponentda value/change yo'q |
Controller / useController |
| xatoda fokus maxsus komponentga o'tmaydi | field.ref ulanmagan |
ref={field.ref} ni DOM elementgacha o'tkazing |
onTouched maxsus komponentda ishlamaydi |
field.onBlur ulanmagan |
onBlur={field.onBlur} |
| har harfda butun forma chiziladi | formada watch(…) |
ko'rinishni alohida komponentga, useWatch |
getValues bilan ko'rsatilgan qiymat eskiradi |
getValues qayta chizmaydi |
ekranda — useWatch |
setValue dan keyin isDirty — false |
sozlama berilmagan | { shouldDirty: true } |
| forma serverdan kelgan ma'lumot bilan to'lmaydi | defaultValues keyin o'zgartirildi |
reset(data) yoki values |
Cannot destructure property 'register' of 'useFormContext(...)' as it is null. |
FormProvider yo'q |
formani <FormProvider {...form}> bilan o'rang |
8. Mashqlar
Mashqlar kurs/mashqlar/19/32-controller/ da. Har yechimni «Ishga tushir» bilan sinang.
1-mashq (oson): qaysi vosita?
Har vazifaga mos RHF vositasini yozing: register, Controller, useWatch, getValues, reset, setValue.
- Oddiy
<input type="email">ni ulash → role="switch"li tugmani ulash →- Narx yozilganda "Jami: …" ni jonli ko'rsatish →
- Tugma bosilganda joriy qiymatlarni konsolga chiqarish →
- Serverdan kelgan taom bilan formani to'ldirish →
- "Bugungi sana" tugmasi sana maydoniga qiymat qo'yadi →
Yechim
register— native input, render yo'q.Controller— tugmadavalueyo'q,onChangega qiymat beriladi.useWatch— ekranda ko'rsatish uchun kuzatish, eng yaxshisi kichik bola komponentda.getValues— hodisa ichida o'qish, qayta chizmaydi.reset— qiymat va boshlang'ich qiymat birga almashadi,isDirtyto'g'ri hisoblanadi.setValue—{ shouldDirty: true }bilan, aks holda "o'zgarish yo'q" deb hisoblanadi.
2-mashq (o'rta): achchiqlik darajasi
«Bahor» buyurtma formasiga "Achchiqlik" tanlovi kerak: uchta tugma — "Yo'q", "O'rtacha", "Achchiq" (aria-pressed). Qiymat — 0, 1 yoki 2. Uni useController bilan SpicePicker komponentiga ajrating va formaga name="spice" bilan ulang (boshlang'ich — 0). Tanlangan daraja ostida jonli ko'rinsin: "Tanlandi: O'rtacha" — useWatch bilan, alohida SpiceLabel komponentida.
import { useForm } from "react-hook-form";
const LEVELS = ["Yo'q", "O'rtacha", "Achchiq"];
export default function App() {
const { handleSubmit } = useForm({ defaultValues: { spice: 0 } });
return (
<form onSubmit={handleSubmit((d) => console.log(d))}>
<p>{LEVELS.join(" / ")}</p>
<button>Saqlash</button>
</form>
);
}Konsolda:
{spice: 0}Yechim
import { useController, useForm, useWatch } from "react-hook-form";
const LEVELS = ["Yo'q", "O'rtacha", "Achchiq"];
function SpicePicker({ control, name }) {
const { field } = useController({ control, name });
return (
<fieldset>
<legend>Achchiqlik</legend>
{LEVELS.map((label, level) => (
<span key={label}>
<button
type="button"
aria-pressed={field.value === level}
onClick={() => field.onChange(level)}
>
{label}
</button>{" "}
</span>
))}
</fieldset>
);
}
function SpiceLabel({ control }) {
const spice = useWatch({ control, name: "spice" });
return <p role="status">Tanlandi: {LEVELS[spice]}</p>;
}
export default function App() {
const { control, handleSubmit } = useForm({
defaultValues: { spice: 0 },
});
return (
<form onSubmit={handleSubmit((d) => console.log(d))}>
<SpicePicker control={control} name="spice" />
<SpiceLabel control={control} />
<button>Saqlash</button>
</form>
);
}Konsolda:
{spice: 1}Ekranda:
Achchiqlik
Yo'q O'rtacha Achchiq
Tanlandi: O'rtacha
SaqlashLEVELS.map((label, level) => …) — map ning ikkinchi argumenti indeks: daraja aynan shu son. field.onChange(level) qiymatning o'zini beradi — hodisa emas. SpiceLabel alohida: tanlov o'zgarganda faqat u va SpicePicker qayta chiziladi.
3-mashq (qiyin): taom tahriri — faqat o'zgarganini yubor
Taom tahrirlash formasi:
fetchDish()(200 ms) —{ name: "Manti", price: "30000", spicy: false }. Kelguncha "Yuklanmoqda…", keyinreset.name,price—register;spicy— almashtirgich,Controllerbilan ("Achchiq").- «Saqlash» —
dirtyFieldsdagi maydonlarnigina konsolga:PATCH {…}. O'zgarish yo'q bo'lsa —O'zgarish yo'q, so'rov yo'q. - Saqlangach forma yangi qiymatlarni "boshlang'ich" deb qabul qilsin: ikkinchi «Saqlash» —
O'zgarish yo'q.
Ishora: 4-band uchun saqlangandan keyin reset(data) — yuborilgan qiymatlar yangi boshlang'ich bo'ladi.
Yechim
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
function fetchDish() {
return new Promise((resolve) =>
setTimeout(
() => resolve({ name: "Manti", price: "30000", spicy: false }),
200,
),
);
}
export default function App() {
const [loading, setLoading] = useState(true);
const {
register,
control,
reset,
handleSubmit,
formState: { dirtyFields, isDirty },
} = useForm({
defaultValues: { name: "", price: "", spicy: false },
});
useEffect(() => {
fetchDish().then((dish) => {
reset(dish);
setLoading(false);
});
}, [reset]);
function save(data) {
if (!isDirty) {
console.log("O'zgarish yo'q");
return;
}
const changed = Object.fromEntries(
Object.keys(dirtyFields).map((key) => [key, data[key]]),
);
console.log("PATCH", changed);
reset(data);
}
if (loading) return <p>Yuklanmoqda…</p>;
return (
<form onSubmit={handleSubmit(save)}>
<p>
<label htmlFor="name">Nomi</label>
<input id="name" {...register("name")} />
</p>
<p>
<label htmlFor="price">Narxi</label>
<input id="price" {...register("price")} />
</p>
<Controller
control={control}
name="spicy"
render={({ field }) => (
<button
ref={field.ref}
type="button"
role="switch"
aria-checked={field.value}
onClick={() => field.onChange(!field.value)}
onBlur={field.onBlur}
>
Achchiq
</button>
)}
/>
<button>Saqlash</button>
</form>
);
}Konsolda:
PATCH {spicy: true}
O'zgarish yo'qBirinchi «Saqlash» — faqat spicy ketdi. reset(data) dan keyin { spicy: true } boshlang'ich qiymat bo'ldi, shuning uchun ikkinchi «Saqlash» — "O'zgarish yo'q". Real ilovada reset server javobi bilan chaqiriladi (server qiymatni o'zgartirgan bo'lishi mumkin — masalan, narxni yaxlitlagan).
4-mashq: Vazifalar qadami
feat/tahrir-formasi ni tugating: ui/Almashtirgich.tsx, Controller va useWatch ni VazifaTahriri.tsx ga, hooklar/useOrqaga.ts, VazifaSahifasi.tsx dagi bo'limlar va Query keshi, CSS, ikki test fayli. Keyin commit.
Yechim
Tekshiruv ro'yxati:
npm test— node:test 190 («Orqaga» tuzatishigacha — 182), Vitest 64.VazifaSahifasi.test.tsx"No QueryClient set" bilan yiqilsa —QueryClientProviderqo'shilmagan («e2e topgan xato» bo'limi).npm run dev:/vazifa/1/tahrir— matnni o'zgartiring, almashtirgichni bosing (●), «Saqlash» →/vazifa/1da sarlavha yangi matn bilan. Eski matn ko'rinsa — sahifa hali loader'dan o'qiyapti.- Havola bilan
/vazifa/1/tahrirni yangi tabda oching → «Bekor qilish» →/vazifa/1; keyin brauzer «Orqaga» — tahrirga qaytmaydi (replace). - Klaviatura: Tab bilan almashtirgichga boring, Space — holat o'zgaradi (tugma — native klaviatura bilan).
git log --oneline -1—feat: /vazifa/:id/tahrir — React Hook Form + Zod, almashtirgich (Controller).
9. Real ishda
- UI kutubxonalar bilan: MUI, Ant Design, shadcn/ui ning
Select,DatePicker,Switchlari —Controller(yokiuseControllerli o'rovchi) bilan ulanadi. shadcn/ui ningFormFieldkomponenti aslidaControllerning o'zi (shadcn/ui I). - Tahrirlash formalari: admin panellarda "faqat o'zgarganini yubor" (
PATCH+dirtyFields) — standart naqsh. U ikki muharrirning bir-birini bosib ketishini kamaytiradi (to'liq yechim — serverdagi versiya tekshiruvi). - Performans: katta formada (100+ maydon) birinchi qadam —
watchlarniuseWatchga va kichik komponentlarga ko'chirish. React DevTools Profiler bilan o'lchab ko'ring (Profiler). - Intervyu savollari: "
registervaControllerfarqi?", "watchvauseWatchfarqi, qaysi biri tezroq?", "tahrirlash formasini serverdan qanday to'ldirasiz?", "dirtyFieldsnima uchun kerak?".
Xulosa
Controller/useController—registerulay olmaydigan komponentlar uchun:field(value,onChange,onBlur,ref) vafieldState.useWatch— faqat o'zi turgan komponentni qayta chizadi;watch— butun formani. Ko'rinishni kichik komponentga qo'ying.setValue— koddan qiymat (shouldDirty,shouldValidatebilan);getValues— hodisada o'qish, render'siz.- Tahrirlash:
reset(data)(yokivalues, loader +defaultValues);dirtyFields— faqat o'zgargan maydonlar. FormProvider+useFormContext— katta formani bo'limlarga ajratish.
Keyingi dars: RHF III: dinamik maydonlar, wizard va async validatsiya — useFieldArray, ko'p qadamli forma, serverda tekshiriladigan qoida, saqlanmagan o'zgarish ogohlantirishi; vazifalar da «Qadamlarga bo'lib qo'shish».
Manbalar
- React Hook Form:
Controller,useController,useWatch,watch,setValue,getValues,reset,useFormContext— react-hook-form.com/docs (2026-10 holatiga, v7.89.0) - React Hook Form:
useForm—values,resetOptions— react-hook-form.com/docs/useform - WAI-ARIA APG: Switch pattern — w3.org/WAI/ARIA/apg/patterns/switch
- React Router:
shouldRevalidate— reactrouter.com/start/data/route-object
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!