IlmHamroh
JavaScript Full-stack/18-qism. React chuqur27/42-dars21 daqiqa
Mundarija (26)

Controlled va uncontrolled komponent API: value, defaultValue va onValueChange

Qisqacha: Yaxshi komponent native <input> kabi ikki rejimda ishlaydi. Uncontrolled: defaultValue beriladi, holatni komponent o'zi saqlaydi. Controlled: value + onValueChange beriladi, holat otada, komponent faqat "o'zgartirmoqchiman" deb xabar beradi. Rejimni bitta qoida hal qiladi: value undefined emasmi — controlled. Bu mantiq bitta kichik hookda yashaydi (useControllableState, kanonda useBoshqariladiganHolat). Undan ham nozik boshqaruv kerak bo'lsa — state reducer: ota har o'zgarishni ko'rib, uni o'zgartira oladi.

Bu darsda

  • Komponent state'i ichkarida yoki otada turishining yutuq va kamchiliklarini ko'rasiz.
  • value / defaultValue / onValueChange uchligini va nomlash odatlarini (open, checked) qo'llaysiz.
  • Ikkala rejimni qo'llaydigan useControllableState hookini yozasiz va rejim almashsa ogohlantirish qo'shasiz.
  • Control props bilan ota o'zgarishni rad eta olishini ko'rasiz.
  • State reducer naqshini tushuntira olasiz; vazifalar dagi Menyu ni controlled va uncontrolled qilasiz.

Oldin bilishingiz kerak: Compound komponentlar va slot naqshi, Formalar I: controlled inputlar, Formalar II: uncontrolled inputlar, useReducer.

1. Nega bu kerak?

Oldingi darsda <Menyu> ni compound komponent qildik. U o'zi ochiladi va yopiladi — holat ichkarida. Ko'p joyda bu ideal: «Hisobotni yuklab olish» menyusini ishlatgan odam "ochiqmi" haqida o'ylamaydi.

Keyin talablar keladi. "Ctrl+E bosilsa, eksport menyusi ochilsin." "Hisobot tayyorlanayotganda menyu yopilmasin." "Ikki menyudan biri ochilsa, ikkinchisi yopilsin." Uchalasida ham ota menyuning holatini bilishi va o'zgartirishi kerak. Holat esa komponent ichida qamalgan. Otaning unga yo'li yo'q.

Teskari xato ham bor. Sardor Menyu ni faqat ota boshqaradigan qildi: <Menyu ochiq={…} onOchiqOzgardi={…}> majburiy. Endi har ishlatilishda otaga useState qo'shish kerak — 20 joyda 20 ta bir xil qator. Hech kimga kerak bo'lmagan ish.

Native <input> bu muammoni allaqachon yechgan (Formalar I, Formalar II):

Rejim Yozilishi Holat qayerda
Uncontrolled <input defaultValue="Osh" /> brauzerda (DOM)
Controlled <input value={name} onChange={…} /> React state'da, otada

Bitta teg — ikki rejim. Kerak bo'lmasa, oddiy. Kerak bo'lsa, to'liq nazorat. Bu dars o'z komponentlaringizni shunday qilishni o'rgatadi.

Hayotiy o'xshatish: konditsioner. Uni o'z pulti bilan boshqarish mumkin — u o'zi "eslab" turadi: 24 daraja. Yoki uni "aqlli uy" tizimiga ulaysiz — endi har buyruq tizim orqali o'tadi. Konditsioner tizimga "harorat o'zgartirilmoqchi" deb xabar beradi, qarorni esa tizim qiladi. Bitta qurilma, ikki rejim.

2. Bir rejimli komponentlar va ularning chegarasi

2.1 Faqat uncontrolled

«Bahor» buyurtma sahifasida "Yetkazib berish" kaliti (switch):

jsx
import { useState } from "react";

function Switch({ label, defaultOn = false }) {
  const [on, setOn] = useState(defaultOn);
  return (
    <button aria-pressed={on} onClick={() => setOn(!on)}>
      {label}: {on ? "ha" : "yo'q"}
    </button>
  );
}

function App() {
  return (
    <div>
      <Switch label="Yetkazib berish" />
      <p>
        <button>Hammasini o'chirish</button>
      </p>
    </div>
  );
}

Ekranda:

text
Yetkazib berish: ha
Hammasini o'chirish

Kalit bosilganda yoqildi. «Hammasini o'chirish» tugmasi esa ishlamaydi — ota kalitning holatiga yeta olmaydi. Uning yagona yo'li — key ni almashtirib komponentni noldan yaratish (State'ning saqlanishi va key). Bu "reset" ishlaydi, lekin "yoq" yoki "o'chir" kabi aniq buyruq bera olmaydi.

2.2 Faqat controlled

jsx
import { useState } from "react";

function Switch({ label, on, onOnChange }) {
  return (
    <button aria-pressed={on} onClick={() => onOnChange(!on)}>
      {label}: {on ? "ha" : "yo'q"}
    </button>
  );
}

function App() {
  const [delivery, setDelivery] = useState(false);
  return (
    <div>
      <Switch
        label="Yetkazib berish"
        on={delivery}
        onOnChange={setDelivery}
      />
      <p>
        <button onClick={() => setDelivery(false)}>
          Hammasini o'chirish
        </button>
      </p>
    </div>
  );
}

Ekranda:

text
Yetkazib berish: yo'q
Hammasini o'chirish

Endi ota hamma narsani boshqaradi: kalit yoqildi, «Hammasini o'chirish» uni o'chirdi. Narxi — Switch ni ishlatgan har joy useState yozishi shart. on ni berib, onOnChange ni unutsangiz — kalit "qotib qoladi", xuddi onChange siz value li input kabi.

Switch ichida endi state yo'q — u sof ko'rinish komponenti. Bu ham to'g'ri dizayn bo'lishi mumkin, agar komponent doim boshqariladigan bo'lsa. Lekin qayta ishlatiladigan UI komponentida ikkala ehtiyoj ham uchraydi.

3. Ikki rejim — bitta komponentda

3.1 Uchlik: value, defaultValue, onValueChange

Qoida native input'dan olingan:

  • value berilgan bo'lsa (undefined emas) — controlled. Komponent holatni props'dan o'qiydi va o'zgartirmoqchi bo'lganda faqat onValueChange(yangi) ni chaqiradi.
  • value berilmagan bo'lsa — uncontrolled. Komponent defaultValue dan boshlaydi va holatni o'zi saqlaydi. onValueChange berilgan bo'lsa ham chaqiriladi — ota "kuzatuvchi" bo'lib turadi.

Nomlar kutubxonalarda deyarli bir xil — Radix, React Aria, Base UI shu odatga keldi:

Holat Controlled Uncontrolled Xabar
Qiymat value defaultValue onValueChange
Ochiqmi open defaultOpen onOpenChange
Belgilanganmi checked defaultChecked onCheckedChange
Tanlangan tab value defaultValue onValueChange

Shuning uchun Switch uchun to'g'ri nomlar: checked, defaultChecked, onCheckedChange. Nima uchun onChange emas? onChange React'da DOM hodisasi bilan bog'langan — u event oladi. onCheckedChange esa tayyor qiymatni oladi: (checked: boolean) => void. Bitta nom ikki xil narsani anglatmasligi uchun shunday ajratiladi. vazifalar kanoni o'zbekcha nomlarda xuddi shu odatni saqlaydi: ochiq, boshlangichOchiq, onOchiqOzgardi.

3.2 useControllableState

Ikki rejim mantig'i har komponentda bir xil. Uni bitta hookka chiqaramiz (Custom hooklar I):

jsx
import { useState } from "react";

function useControllableState(value, defaultValue, onChange) {
  const [inner, setInner] = useState(defaultValue);
  const isControlled = value !== undefined;
  const current = isControlled ? value : inner;
  function setValue(next) {
    if (!isControlled) setInner(next);
    onChange?.(next);
  }
  return [current, setValue];
}

function Switch({
  label,
  checked,
  defaultChecked = false,
  onCheckedChange,
}) {
  const [on, setOn] = useControllableState(
    checked,
    defaultChecked,
    onCheckedChange,
  );
  return (
    <button aria-pressed={on} onClick={() => setOn(!on)}>
      {label}: {on ? "ha" : "yo'q"}
    </button>
  );
}

function App() {
  const [delivery, setDelivery] = useState(false);
  return (
    <div>
      <Switch
        label="Yetkazib berish"
        checked={delivery}
        onCheckedChange={setDelivery}
      />{" "}
      <Switch
        label="Choy sovg'a"
        onCheckedChange={(v) => console.log("sovg'a:", v)}
      />
      <p>
        <button onClick={() => setDelivery(false)}>
          Hammasini o'chirish
        </button>
      </p>
    </div>
  );
}

Konsolda:

text
sovg'a: true

Ekranda:

text
Yetkazib berish: yo'q Choy sovg'a: ha
Hammasini o'chirish

Bitta Switch, ikki ishlatilish. "Yetkazib berish" — controlled: ota uni yoqdi va o'chirdi. "Choy sovg'a" — uncontrolled: o'zi yoqildi, ota esa faqat xabar oldi (sovg'a: true). «Hammasini o'chirish» uni o'chirmadi — ota uni boshqarmaydi. Bu xato emas, foydalanuvchi shunday tanladi.

Hook to'rt qatordan iborat, lekin har qatorida qaror bor:

Qator Nega shunday
useState(defaultValue) controlled rejimda ham chaqiriladi — hooklar tartibi o'zgarmasin (Hooklar qoidalari)
value !== undefined null ham qiymat: "hech narsa tanlanmagan" controlled holat bo'lishi mumkin
if (!isControlled) setInner(next) controlled'da ichki state'ga tegmaymiz — haqiqat manbai bitta
onChange?.(next) ikkala rejimda: uncontrolled'da ham ota kuzata oladi

Controlled va uncontrolled ma'lumot oqimini diagrammada solishtiring:

sequenceDiagram
  participant F as Foydalanuvchi
  participant S as Switch
  participant O as Ota
  F->>S: bosdi
  alt controlled (checked bor)
    S->>O: onCheckedChange(true)
    O->>O: setDelivery(true)
    O->>S: checked=true (yangi props)
  else uncontrolled
    S->>S: setInner(true)
    S-->>O: onCheckedChange(true) — xabar
  end
  S->>F: "ha"

Diagrammaning asosiy farqi: controlled'da Switch o'zini o'zi o'zgartirmaydi. U otaga so'rov yuboradi va yangi props kelishini kutadi. Ota javob bermasa, ekran o'zgarmaydi.

Tekshirib ko'ring: checked={null} berilsa, Switch qaysi rejimda bo'ladi va ekranda nima chiqadi?

Javob

Controlled: null !== undefined. on — null, ekranda "yo'q" (null — falsy). Bosilganda onCheckedChange(true) chaqiriladi, onCheckedChange berilmagan bo'lsa — kalit qotib qoladi. Shuning uchun "bo'sh" qiymatni null bilan ifodalash mumkin bo'lgan komponentlarda (masalan, "hech narsa tanlanmagan" select) bu qaror muhim: null — "ota tanlamadi", undefined — "ota boshqarmaydi".

3.3 Rejim o'rtada almashmasin

Xatolar katalogi dagi A component is changing an uncontrolled input to be controlled ogohlantirishini eslang. Bizning Switch da ham shunday xato bo'lishi mumkin: ota checked={settings.delivery} beradi, settings esa boshida {}. Birinchi renderda undefined — uncontrolled. Keyin true — controlled. Ichki state esa eski qiymat bilan qoladi va keyinroq yana undefined bo'lsa, birdan "tirilib" chiqadi.

Native input bu haqda ogohlantiradi. Yaxshi komponent ham shunday qilishi kerak:

jsx
import { useEffect, useRef, useState } from "react";

function useControllableState(value, defaultValue, onChange) {
  const [inner, setInner] = useState(defaultValue);
  const isControlled = value !== undefined;
  const wasControlled = useRef(isControlled);
  useEffect(() => {
    if (wasControlled.current !== isControlled) {
      const name = (c) => (c ? "controlled" : "uncontrolled");
      const from = name(wasControlled.current);
      const to = name(isControlled);
      console.error(
        `Switch ${from} dan ${to} ga o'tdi — rejim almashmasin`,
      );
      wasControlled.current = isControlled;
    }
  }, [isControlled]);
  const current = isControlled ? value : inner;
  function setValue(next) {
    if (!isControlled) setInner(next);
    onChange?.(next);
  }
  return [current, setValue];
}

function Switch({
  label,
  checked,
  defaultChecked = false,
  onCheckedChange,
}) {
  const [on, setOn] = useControllableState(
    checked,
    defaultChecked,
    onCheckedChange,
  );
  return (
    <button aria-pressed={on} onClick={() => setOn(!on)}>
      {label}: {on ? "ha" : "yo'q"}
    </button>
  );
}

function App() {
  const [settings, setSettings] = useState({}); // ❌ delivery yo'q
  return (
    <Switch
      label="Yetkazib berish"
      checked={settings.delivery}
      onCheckedChange={(v) => setSettings({ delivery: v })}
    />
  );
}

Konsolda:

text
Switch uncontrolled dan controlled ga o'tdi — rejim almashmasin

Ekranda:

text
Yetkazib berish: ha

Ekran to'g'ri ko'rinadi — xato hozircha jim. Lekin ogohlantirish sababni darhol aytadi: useState({ delivery: false }) bo'lishi kerak edi. Ikki detal:

  • Taqqoslash effektda, renderda emas. Ref'ga render paytida yozish render'ni nopok qiladi (useRef I). Effekt esa faqat commit'dan keyin ishlaydi.
  • Ogohlantirish — console.error, throw emas. Rejim almashishi ilovani buzmasligi kerak, faqat dasturchini ogohlantiradi. Radix'ning useControllableState i ham development'da shunga o'xshash ogohlantirish beradi.

3.4 Funksiya bilan yangilash

useState setter'i funksiyani ham qabul qiladi: setOn((prev) => !prev) (updater funksiya). Bizning setValue esa faqat qiymat oladi. Bunga sabab bor: controlled rejimda "oldingi qiymat" — props, va u bir hodisa ichida ikki marta chaqirilsa, ikkinchi chaqiruv eski props'ni ko'radi. Updater qo'llash uchun hook typeof next === "function" ni tekshirib, uni joriy qiymat bilan chaqirishi kerak. Radix shunday qiladi. Lekin controlled rejimda baribir eski props bilan. Kanon soddasini tanladi: Menyu da "ochish" va "yopish" doim aniq qiymat (true/false), updater kerak emas.

4. Control props: ota "yo'q" deya oladi

Controlled rejimning eng kuchli tomoni — ota o'zgarishni rad etishi mumkin. onCheckedChange faqat so'rov, majburiyat emas. Kechki 23:00 dan keyin «Bahor» yetkazib bermaydi:

jsx
import { useState } from "react";

function Switch({ label, checked, onCheckedChange }) {
  return (
    <button
      aria-pressed={checked}
      onClick={() => onCheckedChange(!checked)}
    >
      {label}: {checked ? "ha" : "yo'q"}
    </button>
  );
}

function App() {
  const hour = 23; // soatni o'rnida — misol uchun qotirilgan
  const [delivery, setDelivery] = useState(false);
  const [note, setNote] = useState("");
  function handleChange(next) {
    if (next && hour >= 23) {
      setNote("23:00 dan keyin yetkazib bermaymiz");
      return; // ❗ setDelivery chaqirilmadi — kalit o'zgarmaydi
    }
    setDelivery(next);
  }
  return (
    <div>
      <Switch
        label="Yetkazib berish"
        checked={delivery}
        onCheckedChange={handleChange}
      />
      <p>{note}</p>
    </div>
  );
}

Ekranda:

text
Yetkazib berish: yo'q
23:00 dan keyin yetkazib bermaymiz

Switch "yoqmoqchiman" dedi, ota "yo'q" dedi va sababini ko'rsatdi. Switch o'zgarishsiz qoldi. Bu naqsh control props (boshqaruv props'lari) deyiladi: komponentning ichki holatini props orqali tashqariga chiqarish. Uncontrolled rejimda buning iloji yo'q — komponent avval o'zgaradi, ota esa faqat keyin xabar oladi.

vazifalar dagi «Hisobot tayyorlanayotganda menyu yopilmasin» talabi aynan shunday yechiladi: onOchiqOzgardi={(o) => { if (!tayyorlanmoqda) setOchiq(o); }}.

Tekshirib ko'ring: Uncontrolled Switch ga onCheckedChange={() => false} bersak, o'zgarishni to'xtatib bo'ladimi?

Javob

Yo'q. Uncontrolled rejimda setInner(next) onChange dan oldin va undan qat'i nazar chaqiriladi — qaytarilgan false hech narsani anglatmaydi. To'xtatish uchun ota holatni o'ziga olishi kerak (controlled). Yoki komponent maxsus "rad etish" imkoniyatini berishi kerak — keyingi bo'limdagi state reducer shunday imkoniyat.

5. State reducer: o'zgarishni o'zgartirish

5.1 Muammo

Controlled rejim — "hammasi yoki hech narsa". Ota bitta qoidani qo'shmoqchi bo'lsa ham, butun holatni o'ziga olishi kerak. Murakkab komponentda (avtoto'ldirish, sana tanlagich) holat o'nlab maydondan iborat: ochiqmi, kiritilgan matn, belgilangan qator, tanlangan qiymat. Ularning hammasini otada saqlash — komponentni qaytadan yozish bilan teng.

5.2 Yechim: ota reducer'ni almashtiradi

State reducer naqshi (Kent C. Dodds ommalashtirgan) shunday deydi: komponent ichki holatini useReducer bilan boshqaradi va foydalanuvchiga o'z reducer'ini berish imkonini beradi. Har amal (toggle, reset) avval foydalanuvchi reducer'idan o'tadi. U standart natijani olib, xohlasa o'zgartiradi. Holat esa komponentda qoladi.

«Bahor» aksiyasi: mehmon sovg'a choyni tanlaydi. Aksiya tugagach (buni ota biladi), tanlov qotishi kerak:

jsx
import { useReducer, useState } from "react";

function toggleReducer(state, action) {
  switch (action.type) {
    case "toggle":
      return { on: !state.on };
    case "reset":
      return { on: false };
    default:
      throw new Error(`Noma'lum amal: ${action.type}`);
  }
}

function useToggle({ reducer = toggleReducer } = {}) {
  const [{ on }, dispatch] = useReducer(reducer, { on: false });
  return {
    on,
    toggle: () => dispatch({ type: "toggle" }),
    reset: () => dispatch({ type: "reset" }),
  };
}

function App() {
  const [ended, setEnded] = useState(false);
  const { on, toggle } = useToggle({
    reducer(state, action) {
      if (action.type === "toggle" && ended) {
        return state; // ❗ o'zgarish rad etildi
      }
      return toggleReducer(state, action); // standart xulq
    },
  });
  return (
    <div>
      <button onClick={toggle}>Sovg'a: {on ? "ha" : "yo'q"}</button>
      <p>
        <button onClick={() => setEnded(true)}>Aksiya tugadi</button>
      </p>
      {ended && <p>Tanlov qotdi</p>}
    </div>
  );
}

Ekranda:

text
Sovg'a: ha
Aksiya tugadi
Tanlov qotdi

Birinchi bosish choyni tanladi. «Aksiya tugadi» dan keyin uchinchi bosish rad etildi: tugma "ha" da qoldi. useToggle ichida "aksiya" haqida hech narsa yo'q. Bu qoidani foydalanuvchi o'z reducer'i bilan qo'shdi va toggleReducer ni qayta ishlatdi. Standart reducer eksport qilinishi muhim — aks holda foydalanuvchi butun xulqni qaytadan yozishga majbur bo'lardi.

Nega reducer ended ning yangi qiymatini ko'radi? React navbatdagi amallarni render paytida, shu renderda useReducer ga berilgan reducer bilan qayta ishlaydi. Reducer joriy renderdagi ended ni ko'radi, eskirgan closure bo'lmaydi. Bu nozik joy ham bor: bitta bosishda ham amal, ham setEnded yuborilsa, reducer yangi ended bilan ishlaydi — ikkalasi bitta renderda qayta ishlanadi.

5.3 Naqshlar zinapoyasi

Komponentning holatini tashqaridan qanchalik boshqarish mumkinligi bo'yicha:

Naqsh Foydalanuvchi nima qila oladi Narxi
Uncontrolled boshlang'ich qiymat va kuzatish eng sodda
Controlled (control props) butun holatni boshqarish, rad etish holat otada
State reducer har o'tishni o'zgartirish, holat ichkarida reducer va amal turlarini bilish
Headless xulqni olib, ko'rinishni to'liq o'zi yozish hammasini o'zi chizadi

Har pog'ona ko'proq erkinlik beradi, lekin foydalanuvchidan ko'proq bilim talab qiladi. Downshift (avtoto'ldirish kutubxonasi) to'rttasini ham beradi. Oxirgi pog'ona — Headless komponentlar darsida. Kunlik ishda ko'pincha birinchi ikkitasi yetadi.

5.4 Qaysi holatni tashqariga ochish kerak

Komponentda bir nechta holat bo'lishi mumkin. Masalan, sana tanlagichda: ochiqmi, tanlangan sana, ko'rsatilayotgan oy, klaviatura fokusidagi kun. Hammasini controlled qilish shart emas. Jasur akaning qoidasi uchta savoldan iborat:

  1. Ota bu holatni o'qishi kerakmi? Tanlangan sana — ha, u formaga ketadi. Fokusdagi kun — yo'q, bu faqat klaviatura uchun ichki tafsilot.
  2. Ota uni tashqaridan o'zgartirishi kerakmi? Ochiqmi — ko'pincha ha ("xato bo'lsa, ochiq qolsin"). Ko'rsatilayotgan oy — ba'zan ("bugungi oyga qaytish" tugmasi).
  3. Bu holat boshqa joyda ham yashaydimi? URL, server, localStorage — unda u allaqachon otada. Komponent uni faqat controlled qabul qilsin, aks holda ikki haqiqat manbai paydo bo'ladi (Yagona haqiqat manbai).

Uchala savolga "yo'q" bo'lsa, holat ichkarida qolsin — API kichik bo'ladi. Har ochilgan holat — uchta yangi props (value, defaultValue, onValueChange) va ularni hujjatlashtirish, testlash, keyinchalik buzmasdan saqlash majburiyati. Menyu da faqat bitta holat ochilgan — ochiq. Fokusdagi band ichkarida (#30 da ko'rasiz), chunki uni otaga bildirishning foydasi yo'q.

6. Vazifalar qadami: Menyu — ikki rejim (holat 30)

feat/ui-menyu branch'ida davom etamiz. src/ui/useBoshqariladiganHolat.ts — «useControllableState» bo'limidagi hookning kanondagi varianti:

ts
// useBoshqariladiganHolat.ts — controlled VA uncontrolled (18/#27).
// Qayta ishlatiladigan komponent ikkala rejimni qo'llaydi (native
// <input> kabi: value + onChange YOKI defaultValue):
//  - qiymat berilgan (undefined emas) — controlled: holat otada, biz
//    faqat onOzgardi ni chaqiramiz
//  - berilmagan — uncontrolled: holat shu yerda (boshlang'ichdan)
// Rejim komponent hayoti davomida almashmasin (React ham <input> da
// "controlled → uncontrolled" ogohlantirishi beradi)
import { useState } from "react";

export function useBoshqariladiganHolat<T>(
  qiymat: T | undefined,
  boshlangich: T,
  onOzgardi?: (yangi: T) => void,
): [T, (yangi: T) => void] {
  const [ichki, setIchki] = useState(boshlangich);
  const boshqariladi = qiymat !== undefined;
  const joriy = boshqariladi ? qiymat : ichki;
  function ozgartir(yangi: T) {
    if (!boshqariladi) {
      setIchki(yangi);
    }
    onOzgardi?.(yangi);
  }
  return [joriy, ozgartir];
}

Ikkinchi izoh qatori ikkiga bo'lindi, qolgani aynan kanon. Bizning JavaScript variantidan farqi — turlar: <T> — generic (React + TS IV da komponentlarda). Qaytish turi — aniq kortej [T, (yangi: T) => void], shuning uchun const [ochiq, setOchiq] = … da ikkalasining turi to'g'ri chiqadi. Kanonda rejim almashishi ogohlantirishi yo'q — izohda faqat qoida yozilgan. Menyu ilovada bitta joyda va doim uncontrolled ishlatiladi, shuning uchun kanon qo'shimcha kod qo'shmadi. Agar komponent kutubxonaga chiqsa — «Rejim o'rtada almashmasin» bo'limidagi effekt qo'shiladi.

useMenyu.ts boshida hook shunday ishlatiladi:

ts
type Sozlama = {
  ochiq?: boolean | undefined; // controlled (18/#27)
  boshlangichOchiq?: boolean | undefined; // uncontrolled
  onOchiqOzgardi?: ((ochiq: boolean) => void) | undefined;
};

export function useMenyu({
  ochiq: ochiqProp,
  boshlangichOchiq = false,
  onOchiqOzgardi,
}: Sozlama = {}) {
  const [ochiq, setOchiq] = useBoshqariladiganHolat(
    ochiqProp,
    boshlangichOchiq,
    onOchiqOzgardi,
  );

Nega ochiq?: boolean | undefined — ham ?, ham undefined? vazifalar da exactOptionalPropertyTypes yoqilgan (Strict bayroqlar oilasi): { ochiq?: boolean } ga ochiq: undefined ni aniq yozib bo'lmaydi, faqat tushirib qoldirish mumkin. Menyu esa o'z props'ini doim uzatadi: useMenyu({ ochiq, boshlangichOchiq, onOchiqOzgardi }). Foydalanuvchi ochiq bermasa, u yerga undefined keladi. | undefined shuni ruxsat etadi. Hookning qolgan qismida ochiq va setOchiq oddiy state kabi ishlatiladi — u qaysi rejimda ekanini bilishi shart emas. Mana shu hookning foydasi.

Menyu.tsx dagi props turi:

ts
type MenyuProps = {
  children: ReactNode;
  // Controlled (ochiq + onOchiqOzgardi) yoki uncontrolled
  // (boshlangichOchiq) — 18/#27
  ochiq?: boolean;
  boshlangichOchiq?: boolean;
  onOchiqOzgardi?: (ochiq: boolean) => void;
};

Ilovadagi <Menyu> hech birini bermaydi — uncontrolled. Controlled rejimni test tekshiradi (tekshiruv/Menyu.test.tsx dan):

tsx
  test("controlled (18/#27): ochiq — otadan, onOchiqOzgardi chaqiriladi", () => {
    const ozgardi = vi.fn();
    function Ota() {
      const [ochiq, setOchiq] = useState(true);
      return (
        <Menyu
          ochiq={ochiq}
          onOchiqOzgardi={(o) => {
            ozgardi(o);
            setOchiq(o);
          }}
        >
          <Menyu.Tugma>Yuklab olish</Menyu.Tugma>
          <Menyu.Royxat>
            <Menyu.Band onTanla={() => {}}>CSV</Menyu.Band>
          </Menyu.Royxat>
        </Menyu>
      );
    }
    render(<Ota />);
    expect(tugma().getAttribute("aria-expanded")).toBe("true");
    fireEvent.click(tugma());
    expect(ozgardi).toHaveBeenCalledWith(false);
    expect(tugma().getAttribute("aria-expanded")).toBe("false");
  });

Test ota tomonidan ochilgan menyuni boshlaydi (useState(true)). Tugma bosilganda menyu o'zi yopilmaydi: u onOchiqOzgardi(false) ni chaqiradi. Ota setOchiq(false) qiladi, yangi ochiq={false} keladi, aria-expanded — "false". vi.fn() — chaqiruvlarni yozib oladigan soxta funksiya (mock), Vitest'da — toHaveBeenCalledWith(false) "so'rov yuborildimi" ni tekshiradi. Test nomi 70 belgidan uzun — kanon qatori, o'zgartirilmadi.

Natija holat 30 da: npm test — node 170, Vitest 45 (+6 Menyu testi). Commit — Headless komponentlar darsida, useMenyu bilan birga.

7. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
Komponent bosilsa ham o'zgarmaydi value berilgan, onValueChange yo'q yoki ota setState qilmaydi ikkalasini birga bering; yoki defaultValue
defaultValue o'zgardi, ekran yo'q defaultValue faqat birinchi renderda o'qiladi controlled qiling yoki key bilan qayta yarating
Ota "o'chir" desa ham yoqiq qoladi komponent faqat uncontrolled control props: value qo'shing
Ichki holat "tirilib" chiqadi rejim o'rtada almashdi (undefined ↔ qiymat) boshlang'ich qiymat; dev ogohlantirish
onChange ichida event.target.value yo'q onValueChange hodisa emas, qiymat oladi nomlash odati: on…Change(qiymat)
Controlled'da ikki tez o'zgarish birini yo'qotadi setValue eski props'ni ko'radi qiymat bilan, updater'siz; yoki state reducer

8. Mashqlar

Mashqlar kurs/mashqlar/18/27-controlled-uncontrolled/ da. Har yechimni «Ishga tushir» bilan sinang.

1-mashq (oson): nomlar

Dialog komponenti ochiqligini ikki rejimda qo'llaydi. Nomlarni yozing:

  • controlled qiymat: open
  • uncontrolled boshlang'ich:
  • o'zgarish haqida xabar:
  • Slider uchun boshlang'ich qiymat:
Yechim

Odat: controlled nom — holatning o'zi (open, value, checked). Uncontrolled — default + katta harf (defaultOpen). Xabar — on + nom + Change (onOpenChange). Slider qiymat saqlaydi, shuning uchun value / defaultValue / onValueChange («Uchlik» bo'limidagi jadval).

2-mashq (o'rta): Rating — baho yulduzlari

1 dan 5 gacha baho beradigan Rating yozing: value, defaultValue (standart 0), onValueChange. Har yulduz — tugma, matni 1★ … 5★; tanlangan baho Baho: N. «useControllableState» bo'limidagi hookdan foydalaning. App da ikkita Rating: birinchisi uncontrolled (defaultValue={3}), ikkinchisi controlled — ota bahoni <p>Osh uchun: N</p> da ko'rsatadi.

Kutilgan natija (4★ ikkalasida bosilgandan keyin — avval birinchi, keyin ikkinchi):

text
1★2★3★4★5★ Baho: 4
1★2★3★4★5★ Baho: 4
Osh uchun: 4
Yechim
jsx
import { useState } from "react";

function useControllableState(value, defaultValue, onChange) {
  const [inner, setInner] = useState(defaultValue);
  const isControlled = value !== undefined;
  const current = isControlled ? value : inner;
  function setValue(next) {
    if (!isControlled) setInner(next);
    onChange?.(next);
  }
  return [current, setValue];
}

function Rating({ label, value, defaultValue = 0, onValueChange }) {
  const [rating, setRating] = useControllableState(
    value,
    defaultValue,
    onValueChange,
  );
  return (
    <p aria-label={label}>
      {[1, 2, 3, 4, 5].map((n) => (
        <button
          key={n}
          aria-pressed={n <= rating}
          onClick={() => setRating(n)}
        >
          {n}★
        </button>
      ))}{" "}
      Baho: {rating}
    </p>
  );
}

function App() {
  const [osh, setOsh] = useState(0);
  return (
    <div>
      <Rating label="Xizmat" defaultValue={3} />
      <Rating label="Osh" value={osh} onValueChange={setOsh} />
      <p>Osh uchun: {osh}</p>
    </div>
  );
}

Ekranda:

text
1★2★3★4★5★ Baho: 4
1★2★3★4★5★ Baho: 4
Osh uchun: 4

Birinchi Rating bahoni o'zi saqlaydi: ota uni bilmaydi va bilishi shart emas. Ikkinchisi faqat onValueChange(4) ni chaqirdi — ekrandagi 4 otaning osh state'idan keldi. Harakat qatoridagi ikkinchi bos — CSS selektor: ikkinchi baho guruhidagi 4-tugma (matn bo'yicha bos("4★") doim birinchisini topadi).

3-mashq (qiyin): porsiya — state reducer bilan

usePortion hooki porsiya sonini saqlaydi: inc, dec amallari, boshlang'ich — 1, standart reducer portionReducer eksport qilinadi. Hook ixtiyoriy reducer qabul qilsin. App da shunday reducer bering: porsiya 1 dan kam va 5 dan ko'p bo'lmasin; 5 ga yetganda Ko'pi bilan 5 porsiya yozuvi chiqsin. Hookning o'zida chegara bo'lmasin.

Ishora: «Yechim: ota reducer'ni almashtiradi» bo'limidagi useToggle — xuddi shu tuzilma; Math.min, Math.max (Math).

Kutilgan natija (+ olti marta, keyin − bir marta bosilgandan keyin):

text
− 4 +
Yechim
jsx
import { useReducer } from "react";

export function portionReducer(state, action) {
  switch (action.type) {
    case "inc":
      return { count: state.count + 1 };
    case "dec":
      return { count: state.count - 1 };
    default:
      throw new Error(`Noma'lum amal: ${action.type}`);
  }
}

function usePortion({ reducer = portionReducer } = {}) {
  const [{ count }, dispatch] = useReducer(reducer, { count: 1 });
  return {
    count,
    inc: () => dispatch({ type: "inc" }),
    dec: () => dispatch({ type: "dec" }),
  };
}

function limitReducer(state, action) {
  const next = portionReducer(state, action); // standart xulq
  return { count: Math.min(5, Math.max(1, next.count)) };
}

function App() {
  const { count, inc, dec } = usePortion({ reducer: limitReducer });
  return (
    <div>
      <button onClick={dec}>−</button> {count}{" "}
      <button onClick={inc}>+</button>
      {count === 5 && <p>Ko'pi bilan 5 porsiya</p>}
    </div>
  );
}

Ekranda:

text
− 4 +

limitReducer standart portionReducer ni chaqiradi va natijani 1…5 oralig'iga "qisadi". Olti + dan keyin son 5 da to'xtadi (ekran(…) qadami yozuvni tekshirdi), − dan keyin 4. Reducer komponentdan tashqarida e'lon qilingan: u hech qanday state o'qimaydi, shuning uchun har renderda yangi funksiya yasash shart emas. Hookning o'zi chegaradan bexabar — boshqa sahifada chegarasiz yoki 10 gacha ishlatilishi mumkin.

4-mashq: vazifalar qadami

src/ui/useBoshqariladiganHolat.ts ni yarating va useMenyu ning boshida ishlating («Vazifalar qadami» bo'limi). Menyu ga ochiq, boshlangichOchiq, onOchiqOzgardi props'larini qo'shing. tekshiruv/Menyu.test.tsx ga controlled testini yozing.

Yechim

npm run tip toza bo'lishi uchun Sozlama dagi | undefined larni unutmang — busiz tsc exactOptionalPropertyTypes xatosini beradi (Type 'undefined' is not assignable to type 'boolean'). npx vitest run tekshiruv/Menyu.test.tsx -t controlled bitta testni ishga tushiradi. Ilovaning xulqi o'zgarmaydi — menyu uncontrolled. Shuning uchun brauzer tekshiruvida (e2e) hech narsa o'zgarmasligi kerak.

9. Real ishda

  • UI kutubxonalari: Radix, React Aria, Base UI, MUI, Chakra — hammasida value/defaultValue/onValueChange uchligi va ichkarida useControllableState. Radix'ning hooki alohida paket sifatida ham bor (@radix-ui/react-use-controllable-state).
  • Formalar kutubxonalari: React Hook Form uncontrolled maydonlarni afzal ko'radi, maxsus komponentlar uchun esa Controller beradi — controlled ko'prik (React Hook Form da).
  • State reducer: Downshift (stateReducer), TanStack Table (onStateChange bilan qisman controlled holat) — murakkab komponentlarda.
  • Intervyu savollari: "Controlled va uncontrolled komponent farqi?", "O'z komponentingizni ikki rejimda qanday qilasiz?", "Nega value={undefined} xavfli?", "State reducer naqshi nima uchun kerak?".

Xulosa

  • Qayta ishlatiladigan komponent native input kabi ikki rejimni qo'llasin: value + onValueChange (controlled) yoki defaultValue (uncontrolled).
  • Rejimni value !== undefined hal qiladi; null — controlled "bo'sh" qiymat.
  • Mantiq bitta hookda: useControllableState (kanonda useBoshqariladiganHolat) — controlled'da ichki state'ga tegmaydi, xabarni ikkala rejimda beradi.
  • Rejim komponent umri davomida almashmasin; kutubxonada — development ogohlantirishi.
  • Control props otaga o'zgarishni rad etish imkonini beradi; state reducer — holatni otaga ko'chirmasdan har o'tishni o'zgartirishni.
  • vazifalar holat 30: Menyu — ochiq/boshlangichOchiq/onOchiqOzgardi, controlled rejim test bilan tasdiqlangan.

Keyingi dars: HOC va render props: eski naqshlarni o'qish — hooklardan oldin mantiq qanday ulashilgan va legacy kodda uni qanday taniysiz.

Manbalar

  • React: "Sharing State Between Components" — controlled/uncontrolled — react.dev/learn/sharing-state-between-components
  • React: useReducer — react.dev/reference/react/useReducer
  • Radix: useControllableState — github.com/radix-ui/primitives (packages/react/use-controllable-state)
  • Kent C. Dodds: "The State Reducer Pattern with React Hooks" — kentcdodds.com/blog/the-state-reducer-pattern-with-react-hooks
  • Kanon: sinov/18/QADAMLAR.md (#26–#30), holatlar/30/src/ui/useBoshqariladiganHolat.ts, tekshiruv/Menyu.test.tsx
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Controlled va uncontrolled komponent API: value, defaultValue va onValueChange — IlmHamroh