React Hook Form — murakkab formalar, massiv maydonlar

~25 daq · React · Formalar

Muammodan boshlaylik

Rezyume formasi:

  • Ish tajribasi — foydalanuvchi "+ Tajriba qo'shish" tugmasi bilan istalgancha blok qo'shadi: kompaniya, lavozim, yillar. Har birini o'chirish yoki tartibini o'zgartirish mumkin
  • Shahar — tayyor UI kutubxonasidagi chiroyli Select komponenti. U ref ni qabul qilmaydi, onChange da event emas, qiymat qaytaradi
  • Bir necha qadam — 1-qadam: shaxsiy ma'lumotlar, 2-qadam: tajriba, 3-qadam: ko'rib chiqish

register bularning birortasiga ham to'g'ridan-to'g'ri mos emas. React Hook Form da ular uchun maxsus vositalar bor: useFieldArray, Controller va FormProvider.


useFieldArray — dinamik ro'yxat

import { useForm, useFieldArray } from 'react-hook-form';

function Rezyume() {
  const { register, control, handleSubmit } = useForm({
    defaultValues: {
      ism: '',
      tajriba: [{ kompaniya: '', lavozim: '', yillar: '' }],
    },
  });

  const { fields, append, remove, move } = useFieldArray({
    control,
    name: 'tajriba',
  });

  return (
    <form onSubmit={handleSubmit(console.log)}>
      <input {...register('ism')} placeholder="Ism" />

      <fieldset>
        <legend>Ish tajribasi</legend>

        {fields.map((field, index) => (
          <div key={field.id} className="tajriba-blok">
            <input {...register(`tajriba.${index}.kompaniya`)} placeholder="Kompaniya" />
            <input {...register(`tajriba.${index}.lavozim`)} placeholder="Lavozim" />
            <input
              type="number"
              {...register(`tajriba.${index}.yillar`, { valueAsNumber: true })}
              placeholder="Yillar"
            />

            <button type="button" onClick={() => move(index, index - 1)} disabled={index === 0}>
              ↑
            </button>
            <button type="button" onClick={() => remove(index)} aria-label={`${index + 1}-tajribani o'chirish`}>
              ✕
            </button>
          </div>
        ))}

        <button
          type="button"
          onClick={() => append({ kompaniya: '', lavozim: '', yillar: '' })}
        >
          + Tajriba qo'shish
        </button>
      </fieldset>

      <button type="submit">Saqlash</button>
    </form>
  );
}

Natija:

{
  ism: 'Ali',
  tajriba: [
    { kompaniya: 'Uzum', lavozim: 'Frontend', yillar: 2 },
    { kompaniya: 'Payme', lavozim: 'Junior', yillar: 1 },
  ],
}

Asosiy qoidalar

Qoida Nima uchun
key={field.id} — index emas RHF har bir elementga barqaror id beradi; index bilan o'chirish va ko'chirishda qiymatlar aralashib ketadi (27-dars)
Nom: `tajriba.${index}.kompaniya` Massiv ichidagi maydon manzili
append ga to'liq obyekt bering Barcha maydonlar bilan — append({}) emas
Tugmalarga type="button" Aks holda formani yuboradi (62-dars)

Metodlar

Metod Vazifasi
append(obj) Oxiriga qo'shish
prepend(obj) Boshiga
insert(i, obj) Berilgan joyga
remove(i) O'chirish (remove() — hammasini)
move(from, to) / swap(a, b) Tartibni o'zgartirish
update(i, obj) Elementni almashtirish
replace(arr) Butun ro'yxatni almashtirish

Zod bilan

const rezyumeSchema = z.object({
  ism: z.string().min(2),
  tajriba: z
    .array(
      z.object({
        kompaniya: z.string().min(1, 'Kompaniyani kiriting'),
        lavozim: z.string().min(1, 'Lavozimni kiriting'),
        yillar: z.coerce.number().min(0).max(50),
      })
    )
    .min(1, 'Kamida bitta tajriba qo\'shing'),
});
{errors.tajriba?.[index]?.kompaniya && (
  <p role="alert">{errors.tajriba[index].kompaniya.message}</p>
)}

{errors.tajriba?.root && <p role="alert">{errors.tajriba.root.message}</p>}

Massivning o'ziga tegishli xato (min(1)) — errors.tajriba.root da.


Controller — tashqi komponentlar

register oddiy HTML input lar uchun ishlaydi: ref bor, onChange(event) bor. UI kutubxonalari (MUI, Ant Design, React Select, sana tanlagichlar) esa ko'pincha boshqacha:

<Select value={...} onChange={(tanlangan) => ...} />     // event emas, qiymat
<DatePicker selected={...} onChange={(sana) => ...} />

Ular uchun — Controller:

import { Controller } from 'react-hook-form';
import Select from 'react-select';

const shaharlar = [
  { value: 'toshkent', label: 'Toshkent' },
  { value: 'samarqand', label: 'Samarqand' },
  { value: 'buxoro', label: 'Buxoro' },
];

<Controller
  name="shahar"
  control={control}
  rules={{ required: 'Shaharni tanlang' }}
  render={({ field, fieldState }) => (
    <>
      <Select
        inputId="shahar"
        options={shaharlar}
        value={shaharlar.find((s) => s.value === field.value) ?? null}
        onChange={(tanlangan) => field.onChange(tanlangan?.value)}
        onBlur={field.onBlur}
        ref={field.ref}
      />
      {fieldState.error && <p role="alert">{fieldState.error.message}</p>}
    </>
  )}
/>
  • field — value, onChange, onBlur, name, ref; ularni komponentning o'z prop lariga moslang
  • fieldState — error, isTouched, isDirty
  • field.onChange(qiymat) ga toza qiymat bering — forma ma'lumotida { value, label } obyekti emas, 'toshkent' saqlansin

Controller — controlled rejim: bu maydon o'zgarganda render bo'ladi. Shuning uchun uni faqat kerakli joyda ishlating; oddiy input lar uchun register.

O'z komponentingiz — useController

import { useController } from 'react-hook-form';

function YulduzBaho({ name, control }) {
  const { field } = useController({ name, control, rules: { min: 1 } });

  return (
    <div role="radiogroup" aria-label="Baho">
      {[1, 2, 3, 4, 5].map((n) => (
        <button
          key={n}
          type="button"
          role="radio"
          aria-checked={field.value === n}
          onClick={() => field.onChange(n)}
        >
          {n <= field.value ? '★' : '☆'}
        </button>
      ))}
    </div>
  );
}

Qayta ishlatiladigan maydon komponentlari uchun Controller ning hook ko'rinishi.


FormProvider — chuqur komponentlar

Katta formani kichik komponentlarga bo'lsangiz, register, control, errors ni har biriga props bilan uzatish kerak bo'ladi — prop drilling. Yechim — Context:

import { useForm, FormProvider, useFormContext } from 'react-hook-form';

function Rezyume() {
  const methods = useForm({ resolver: zodResolver(rezyumeSchema) });

  return (
    <FormProvider {...methods}>
      <form onSubmit={methods.handleSubmit(saqla)}>
        <ShaxsiyMalumotlar />
        <TajribaBolimi />
        <button type="submit">Saqlash</button>
      </form>
    </FormProvider>
  );
}

function ShaxsiyMalumotlar() {
  const { register, formState: { errors } } = useFormContext();

  return (
    <fieldset>
      <legend>Shaxsiy ma'lumotlar</legend>
      <input {...register('ism')} />
      {errors.ism && <p role="alert">{errors.ism.message}</p>}
    </fieldset>
  );
}

useFormContext() — useForm qaytargan hamma narsani beradi.


Ko'p qadamli forma

const qadamMaydonlari = [
  ['ism', 'email', 'telefon'],        // 1-qadam
  ['tajriba'],                        // 2-qadam
  [],                                 // 3-qadam — ko'rib chiqish
];

function KopQadamliForma() {
  const [qadam, setQadam] = useState(0);
  const methods = useForm({
    resolver: zodResolver(rezyumeSchema),
    mode: 'onTouched',
    shouldUnregister: false,           // yashirilgan qadamlar qiymatlari saqlansin (standart)
  });

  async function keyingi() {
    const togri = await methods.trigger(qadamMaydonlari[qadam]);   // faqat shu qadamni tekshirish
    if (togri) setQadam((q) => q + 1);
  }

  return (
    <FormProvider {...methods}>
      <form onSubmit={methods.handleSubmit(yuborish)}>
        <p aria-live="polite">{qadam + 1} / 3-qadam</p>

        {qadam === 0 && <ShaxsiyMalumotlar />}
        {qadam === 1 && <TajribaBolimi />}
        {qadam === 2 && <KoribChiqish />}

        {qadam > 0 && (
          <button type="button" onClick={() => setQadam((q) => q - 1)}>Orqaga</button>
        )}
        {qadam < 2 ? (
          <button type="button" onClick={keyingi}>Keyingi</button>
        ) : (
          <button type="submit">Yuborish</button>
        )}
      </form>
    </FormProvider>
  );
}
  • trigger(['ism', 'email']) — faqat ko'rsatilgan maydonlarni tekshiradi; Promise<boolean> qaytaradi
  • Qadamlar orasida qiymatlar saqlanadi — bitta useForm, faqat ko'rinadigan qism almashadi
  • Ko'rib chiqish qadamida — getValues() bilan barcha ma'lumotni ko'rsatish

[!TIP] Uzun formalarda foydalanuvchi ma'lumotini yo'qotmasligi uchun qoralamani saqlang: watch bilan o'zgarishlarni kuzatib, debounce bilan localStorage ga yozing va useForm({ defaultValues: saqlangan }) bilan qaytaring. Sahifa tasodifan yopilsa ham — ish yo'qolmaydi.


Nima uchun shunday?

Nima uchun useFieldArray da key uchun index emas, field.id kerak — elementlar tartibi baribir index bilan aniqlanadi-ku?

Chunki key — React uchun elementning shaxsi, uning tartibi emas. Ikkinchi tajriba blokini o'chirsangiz, uchinchisi ikkinchi o'ringa siljiydi. key={index} bo'lsa, React "1-indexli element hali ham bor, faqat oxirgisi yo'qoldi" deb o'ylaydi va DOM dagi input larni joyida qoldiradi — natijada o'chirilgan blokning qiymatlari ekranda qoladi, oxirgi blok esa yo'qoladi. RHF qiymatlarni DOM dan o'qigani uchun (63-dars) bu xato ayniqsa og'ir: ekranda bir narsa, forma ma'lumotida boshqa narsa. field.id — har bir element yaratilganda beriladigan va u bilan birga ko'chadigan barqaror identifikator; React uni kuzatadi va DOM ni to'g'ri yangilaydi.


Xulosa

Eslab qoling:

  • useFieldArray({ control, name }) → fields, append, remove, move, insert, update
  • key={field.id} — index emas; nomlar: `tajriba.${index}.kompaniya`
  • append ga to'liq obyekt; tugmalarga type="button"
  • Massiv xatolari: errors.tajriba?.[i]?.maydon, errors.tajriba?.root
  • Controller / useController — ref siz yoki qiymat qaytaradigan tashqi komponentlar uchun
  • FormProvider + useFormContext — katta formani komponentlarga bo'lish
  • Ko'p qadamli forma — bitta useForm, trigger(maydonlar) bilan qadamni tekshirish

Keyingi bo'lim

10-bo'lim: Performance — React qanday render qiladi va ilovani qanday tezlashtirish mumkin.