IlmHamroh
JavaScript Full-stack/19-qism. React: routing, server state, formalar32/36-dars25 daqiqa
Mundarija (29)

React Hook Form Controller, useWatch va tahrirlash formasi: reset, dirtyFields, useFormContext

Qisqacha: register faqat haqiqiy <input> bilan ishlaydi. O'z komponentingiz (switch, combobox, UI kutubxona inputi) — Controller yoki useController bilan ulanadi: RHF unga value, onChange, onBlur, ref beradi. 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 formani useFormContext bilan oladi.

Bu darsda

  • Maxsus komponentni Controller va useController bilan formaga ulaysiz, field va fieldState ni ishlatasiz.
  • watch va useWatch ning render farqini o'lchaysiz, setValue va getValues ni qachon ishlatishni bilasiz.
  • Serverdan kelgan ma'lumotni reset bilan formaga qo'yasiz va faqat dirtyFields ni yuborasiz.
  • Katta formani bo'laklarga FormProvider va useFormContext bilan ajratasiz.
  • vazifalar tahrir 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:

  1. "Bajarildi" — almashtirgich (switch) bo'lsin, katak emas. Sardor uni <button role="switch"> bilan yasadi. Tugmada value yo'q — register uni o'qiy olmaydi.
  2. Matn yozilayotganda teglar jonli ko'rinsin: "Teglar: #bozor". Lekin RHF harf yozilganda qayta chizmaydi — qanday ko'rsatamiz?
  3. 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:

jsx
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:

text
{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):

jsx
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:

text
{done: true}

Ekranda:

text
● Bajarildi
Saqlash

To'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):

jsx
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:

text
Qoidalarga roziman Qoidalarga rozilik kerak
Yuborish

Xato 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:

jsx
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:

text
{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 ham Controller bilan 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:

jsx
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:

text
Matn
Ko'rinish: Osh
Forma chizildi: 5
jsx
import { 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:

text
Matn
Ko'rinish: Osh
Forma chizildi: 2

Natija 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:

jsx
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:

text
{dish: "Lag'mon"}

Ekranda:

text
Taom
Osh Lag'mon Tekshir
isDirty: true

setValue 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. onSubmit dagi data.dish nima? isDirty nima?

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:

jsx
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:

text
PATCH {price: '37000'}

Ekranda:

text
Nomi
Narxi
O'zgargan: ha
Saqlash

Qadamma-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:

jsx
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:

text
{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):

tsx
// 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):

tsx
      {/* 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:

tsx
  // useWatch (19/#32): faqat shu qiymat o'zgarganda — teglar
  // ko'rinishi
  const matn = useWatch({ control, name: "matn" });
  const teglar = [...teglariniOl(matn)];

JSX'da:

tsx
      <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):

ts
// 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/tahrir ni ochib, «Bekor qilish» ni bossa, /vazifa/2 ga o'tadi. Keyin brauzerning «Orqaga» tugmasi uni yana tahrirga qaytarmasligi kerak — replace shuni ta'minlaydi.

VazifaSahifasi.tsx dagi OrqagaTugmasi ham endi shu hook'ni ishlatadi (kanon diff):

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):

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:

diff
-  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):

tsx
    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, Vitest Tests 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 /kirish tuzatishlari bilan 81). Tahrir formasiga tegishlilari:
text
✅ 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.

bash
git add -A
git commit

Commit (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 — tugmada value yo'q, onChange ga 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, isDirty to'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.

jsx
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:

text
{spice: 0}
Yechim
jsx
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:

text
{spice: 1}

Ekranda:

text
Achchiqlik
Yo'q O'rtacha Achchiq
Tanlandi: O'rtacha
Saqlash

LEVELS.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:

  1. fetchDish() (200 ms) — { name: "Manti", price: "30000", spicy: false }. Kelguncha "Yuklanmoqda…", keyin reset.
  2. name, price — register; spicy — almashtirgich, Controller bilan ("Achchiq").
  3. «Saqlash» — dirtyFields dagi maydonlarnigina konsolga: PATCH {…}. O'zgarish yo'q bo'lsa — O'zgarish yo'q, so'rov yo'q.
  4. 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
jsx
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:

text
PATCH {spicy: true}
O'zgarish yo'q

Birinchi «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 — QueryClientProvider qo'shilmagan («e2e topgan xato» bo'limi).
  • npm run dev: /vazifa/1/tahrir — matnni o'zgartiring, almashtirgichni bosing (●), «Saqlash» → /vazifa/1 da sarlavha yangi matn bilan. Eski matn ko'rinsa — sahifa hali loader'dan o'qiyapti.
  • Havola bilan /vazifa/1/tahrir ni 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, Switch lari — Controller (yoki useController li o'rovchi) bilan ulanadi. shadcn/ui ning FormField komponenti aslida Controller ning 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 — watch larni useWatch ga va kichik komponentlarga ko'chirish. React DevTools Profiler bilan o'lchab ko'ring (Profiler).
  • Intervyu savollari: "register va Controller farqi?", "watch va useWatch farqi, qaysi biri tezroq?", "tahrirlash formasini serverdan qanday to'ldirasiz?", "dirtyFields nima uchun kerak?".

Xulosa

  • Controller / useController — register ulay olmaydigan komponentlar uchun: field (value, onChange, onBlur, ref) va fieldState.
  • useWatch — faqat o'zi turgan komponentni qayta chizadi; watch — butun formani. Ko'rinishni kichik komponentga qo'ying.
  • setValue — koddan qiymat (shouldDirty, shouldValidate bilan); getValues — hodisada o'qish, render'siz.
  • Tahrirlash: reset(data) (yoki values, 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
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
React Hook Form Controller, useWatch va tahrirlash formasi: reset, dirtyFields, useFormContext — IlmHamroh