IlmHamroh
JavaScript Full-stack/17-qism. React asoslari11/45-dars22 daqiqa
Mundarija (34)

Hodisalarni boshqarish: React'da onClick, onChange va onSubmit — funksiyani uzating, chaqirmang

Qisqacha: React'da hodisa ishlovchisi JSX atributi bilan ulanadi: <button onClick={handleClick}>. Unga funksiyaning o'zi beriladi — handleClick, chaqiruvi handleClick() emas: aks holda u chizish paytida bajariladi. Argument kerak bo'lsa — arrow funksiya: onClick={() => removeDish(dish.id)}. Ishlovchi brauzer hodisasining o'rami — SyntheticEvent — ni oladi: preventDefault(), stopPropagation(), target odatdagidek ishlaydi. React'ning onChange i har harfda ishga tushadi (brauzerdagi change dan farqli). Nomlash: prop — onDelete, ishlovchi — handleDelete.

Bu darsda

  • onClick, onChange, onSubmit, onKeyDown ishlovchilarini ulaysiz.
  • onClick={f} va onClick={f()} farqini tushuntira olasiz va argumentni arrow bilan uzatasiz.
  • preventDefault, stopPropagation va capture fazasini React'da ishlatasiz.
  • Hodisa ishlovchisini props bilan pastga uzatasiz va to'g'ri nomlaysiz.
  • vazifalar qatorlaridagi delegatsiyani React hodisalariga almashtirasiz.

Oldin bilishingiz kerak: Pure komponentlar va render qoidasi, addEventListener asoslari, Hodisa tarqalishi: capture va bubble, Event delegation.

1. Nega bu kerak?

Pure komponentlar darsida qoida bor edi: render faqat hisoblaydi, ish esa boshqa joyda bajariladi. O'sha joy — hodisa ishlovchisi. Tugma bosildi — buyurtma qo'shiladi. Maydonga yozildi — qidiruv yangilanadi. Forma yuborildi — ma'lumot serverga ketadi.

Vanilla'da buni addEventListener bilan qilardik. vazifalar v6 da esa butun ro'yxatga bitta tinglovchi qo'yilgan edi (Event delegation). U har bosishda closest("[data-amal]") bilan qaysi tugma ekanini qidirib, switch bilan tanlardi. Bu ishlaydi, lekin tugma va uning vazifasi ikki xil faylda yashaydi: <button data-amal="ochir"> — render.ts da, "o'chir" ning ma'nosi — asosiy.ts dagi switch da.

React'da tugma va uning ishi bir joyda yoziladi: <button onClick={…}>. Hayotiy o'xshatish: restorandagi chaqiruv tugmasi. Vanilla delegatsiya — zaldagi bitta umumiy qo'ng'iroq: ofitsiant kelib, "qaysi stoldan?" deb so'raydi. React — har stolga o'z tugmasi: bosildi — ofitsiant aynan qayerga borishni biladi.

2. Birinchi ishlovchi

2.1 onClick

jsx
function OrderButton() {
  function handleClick() {
    console.log("Buyurtma qabul qilindi!");
  }

  return <button onClick={handleClick}>Buyurtma berish</button>;
}

function App() {
  return <OrderButton />;
}

Konsolda:

text
Buyurtma qabul qilindi!

Ekranda:

text
Buyurtma berish

Uch narsa. Atribut nomi — camelCase onClick (JSX I da kichik harfli onclick ishlamasligini ko'rgandik). Qiymati — {} ichida funksiya. Funksiya komponent ichida e'lon qilingan — demak u komponentning props'ini ko'radi (closure).

Qisqa ishlovchini to'g'ridan-to'g'ri yozish ham mumkin: onClick={() => console.log("…")}. Bir-ikki qatorlik ish uchun — qulay. Uzunrog'i alohida funksiyada o'qilishi oson.

2.2 f va f() — eng ko'p uchraydigan xato

jsx
function sayHello() {
  console.log("Salom!");
}

function App() {
  return <button onClick={sayHello()}>Salom</button>; // ❌ () bor
}

Konsolda:

text
Salom!

Ekranda:

text
Salom

Konsolda bitta "Salom!" bor — lekin u bosishdan emas. {sayHello()} — funksiya chizish paytida chaqirildi, uning natijasi (undefined) esa onClick ga berildi. Blok tugmani bosdi, lekin hech narsa bo'lmadi: ishlovchi yo'q. StrictMode'da "Salom!" chizishning o'zida ikki marta chiqardi.

Yozuv Nima uzatiladi Qachon ishlaydi
onClick={sayHello} funksiyaning o'zi bosilganda
onClick={() => sayHello()} yangi funksiya bosilganda
onClick={sayHello()} chaqiruv natijasi chizish paytida

Bu xato ba'zan jim emas. Ishlovchi state'ni o'zgartirsa (keyingi dars), chizish → o'zgartirish → yana chizish zanjiri cheksiz aylanadi va React to'xtatadi. TypeScript esa uni darhol ushlaydi:

tsx
function sayHello() {
  console.log("Salom!");
}

export function App() {
  return <button onClick={sayHello()}>Salom</button>;
}

Xato:

text
TS2322: Type 'void' is not assignable to type 'MouseEventHandler<HTMLButtonElement> | undefined'.

"void turini MouseEventHandler<HTMLButtonElement> ga berib bo'lmaydi" — ya'ni onClick funksiya kutyapti, siz esa "hech narsa" (sayHello() natijasi) berdingiz.

2.3 Argument uzatish

Ro'yxatda har tugma o'z taomini o'chirishi kerak. Ishlovchiga id kerak — lekin onClick={removeDish(dish.id)} — o'sha f() xatosi. Yechim — arrow funksiya bilan "o'rash":

jsx
function DishList({ dishes, onRemove }) {
  return (
    <ul>
      {dishes.map((dish) => (
        <li key={dish.id}>
          {dish.name}{" "}
          <button
            aria-label={`O'chirish: ${dish.name}`}
            onClick={() => onRemove(dish.id)}
          >
            ×
          </button>
        </li>
      ))}
    </ul>
  );
}

function App() {
  const dishes = [
    { id: 1, name: "Osh" },
    { id: 2, name: "Manti" },
  ];
  return (
    <DishList
      dishes={dishes}
      onRemove={(id) => console.log("O'chirilsin:", id)}
    />
  );
}

Konsolda:

text
O'chirilsin: 2

Ekranda:

text
Osh ×
Manti ×

() => onRemove(dish.id) — har qator uchun yangi funksiya. U chizishda yaratiladi, lekin chaqirilmaydi. Bosilganda esa o'zi eslab qolgan dish.id bilan onRemove ni chaqiradi (closure). Blok "Manti" tugmasini aria-label orqali topdi — Malika ham ekran o'quvchida aynan shuni eshitadi.

DishList o'chirishni o'zi qilmaydi — onRemove prop'ini chaqiradi. "Nima bo'ldi" ni bola aytadi, "nima qilish" ni ota hal qiladi. Bu Props darsidagi "funksiya ham prop" ning eng muhim ishlatilishi. Hozircha ro'yxat o'zgarmaydi — o'chirish uchun ma'lumot o'zgarishi kerak, buni useState bilan qilamiz.

Tekshirib ko'ring: <button onClick={console.log("bosildi")}> — nima bo'ladi?

Javob

console.log("bosildi") chizish paytida bir marta (StrictMode'da ikki marta) ishlaydi va undefined qaytaradi — onClick={undefined}. Bosilganda hech narsa bo'lmaydi. To'g'risi: onClick={() => console.log("bosildi")}.

3. Hodisa obyekti

3.1 SyntheticEvent

Ishlovchi birinchi argument sifatida hodisa obyektini oladi — vanilla'dagidek (Event obyekti). Lekin bu brauzerning asl hodisasi emas, React'ning o'rami — SyntheticEvent ("sun'iy hodisa"). Uning maydonlari brauzerdagidek nomlangan, hamma brauzerda bir xil ishlaydi. Asl hodisa kerak bo'lsa — event.nativeEvent:

jsx
function App() {
  function handleClick(event) {
    console.log(event.type, event.target.tagName);
    console.log("asli:", event.nativeEvent instanceof MouseEvent);
  }
  return <button onClick={handleClick}>Savatga</button>;
}

Konsolda:

text
click BUTTON
asli: true

Ekranda:

text
Savatga

Eski kodda event.persist() ni uchratasiz — React 16 gacha hodisa obyekti qayta ishlatilardi. React 17 dan beri bu kerak emas, persist() hech narsa qilmaydi.

3.2 onChange — har harfda

Bu yerda React vanilla'dan farq qiladi. Brauzerdagi change hodisasi maydon fokusni yo'qotganda ishlaydi, input — har harfda (input va change). React'ning onChange i esa har harfda ishlaydi — aslida u brauzerning input hodisasiga ulangan:

jsx
function App() {
  function handleChange(event) {
    console.log(event.target.value, "|", event.nativeEvent.type);
  }
  return <input aria-label="Taom" onChange={handleChange} />;
}

Konsolda:

text
Osh | input

Ekranda:

text
(bo'sh)

nativeEvent.type — "input". Blok matnni bir harakatda yozdi, shuning uchun bitta qator. Klaviaturada yozsangiz — har harf uchun bittadan. React jamoasi bu nomni tarixan tanlagan: "maydon o'zgardi" — har o'zgarishda. Vanilla'dan kelganlar uchun bu kutilmagan, shuning uchun eslab qoling: React onChange = brauzer input.

3.3 onSubmit va preventDefault

Forma yuborilganda brauzer sahifani qayta yuklaydi. SPA'da bunga yo'l qo'yilmaydi — vanilla'dagidek preventDefault() (standart harakatlar):

jsx
function App() {
  function handleSubmit(event) {
    event.preventDefault(); // sahifa qayta yuklanmasin
    const data = new FormData(event.currentTarget);
    console.log("Yangi taom:", data.get("dish"));
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="dish" aria-label="Taom" />
      <button type="submit">Qo'shish</button>
    </form>
  );
}

Konsolda:

text
Yangi taom: Osh

Ekranda:

text
Qo'shish

onSubmit — <form> da, tugmada emas. Shunda Enter bosilganda ham ishlaydi (Bobur faqat klaviatura bilan ishlaydi). event.currentTarget — ishlovchi ulangan element, ya'ni forma. FormData uni bir qatorda o'qiydi. React'dagi formalar mavzusi katta — Formalar I va Formalar II darslarida. U yerda React 19 ning <form action> imkoniyati ham bor.

Diqqat: React'da return false standart harakatni to'xtatmaydi (eski jQuery va onclick="return false" odati). Faqat event.preventDefault().

4. Tarqalish: bubble, capture va stopPropagation

4.1 Hodisa yuqoriga ko'tariladi

React hodisalari ham bubble qiladi: ichki elementdagi bosish otalarga ham yetadi. Capture fazasi uchun — nom oxirida Capture:

jsx
function App() {
  return (
    <div
      onClickCapture={() => console.log("1. div — capture")}
      onClick={() => console.log("3. div — bubble")}
    >
      <button onClick={() => console.log("2. button")}>
        Savatga
      </button>
    </div>
  );
}

Konsolda:

text
1. div — capture
2. button
3. div — bubble

Ekranda:

text
Savatga

Tartib brauzerdagidek: avval tepadan pastga (capture), keyin nishon, keyin pastdan tepaga (bubble).

Faza Vanilla React
capture addEventListener("click", f, true) onClickCapture={f}
nishon va bubble addEventListener("click", f) onClick={f}
to'xtatish event.stopPropagation() event.stopPropagation()

4.2 stopPropagation

Taom kartasi butunlay bosiladigan: bosilsa, tafsilotlar ochiladi. Ichida "Savatga" tugmasi bor. Tugma bosilganda karta ochilmasligi kerak:

jsx
function DishCard() {
  return (
    <div onClick={() => console.log("Karta ochildi")}>
      <h3>Osh</h3>
      <button
        onClick={(event) => {
          event.stopPropagation();
          console.log("Savatga qo'shildi");
        }}
      >
        Savatga
      </button>
    </div>
  );
}

function App() {
  return <DishCard />;
}

Konsolda:

text
Savatga qo'shildi

Ekranda:

text
Osh
Savatga

"Karta ochildi" chiqmadi — hodisa tugmada to'xtadi. stopPropagation ni ehtiyotkorlik bilan ishlating: yuqoridagi boshqa tinglovchilar (masalan, analitika yoki "tashqarida bosilsa menyuni yop") ham hodisani ko'rmay qoladi.

4.3 Ichkarida React ham delegatsiya qiladi

Qiziq fakt: React har onClick uchun alohida addEventListener qo'ymaydi. U ilova ildiziga (createRoot olgan elementga) har hodisa turidan bittadan tinglovchi qo'yadi. Hodisa kelganda, qaysi komponentning ishlovchisini chaqirishni o'zi topadi.

flowchart LR
  B["button: bosildi"] -- "bubble" --> U["ul"]
  U -- "bubble" --> R["#root: React tinglovchisi"]
  R --> F["komponentlar daraxtidan yo'l"]
  F --> H["onClick'lar chaqiriladi: ichkaridan tashqariga"]

Brauzer hodisasi DOM bo'ylab ildizgacha ko'tariladi. U yerda React uni ushlaydi, o'z komponentlar daraxtidan bosilgan elementgacha yo'lni topadi va yo'ldagi onClick larni tartib bilan chaqiradi. Ya'ni v6 dagi delegatsiya yo'qolmadi — u React'ning ichiga ko'chdi. Siz uchun esa har tugma o'z ishlovchisi bilan yoziladi.

Buning bitta amaliy oqibati bor. dispatchEvent bilan sun'iy hodisa yuborayotgan kod (masalan, testlar) bubbles: true ni unutsa — hodisa ildizga yetib bormaydi. Unda React uni ko'rmaydi. vazifalar ning brauzer tekshiruvida aynan shu tuzoq uchraydi — State III darsida.

5. Klaviatura hodisalari

onKeyDown — vanilla keydown (Klaviatura hodisalari). event.key — bosilgan tugma nomi:

jsx
function App() {
  function handleKeyDown(event) {
    if (event.key === "Escape") {
      event.currentTarget.value = "";
      console.log("Tozalandi");
    }
  }

  return (
    <>
      <input aria-label="Qidiruv" onKeyDown={handleKeyDown} />
      <button
        id="tozala"
        onClick={() => {
          const input = document.querySelector("input");
          const esc = new KeyboardEvent("keydown", {
            key: "Escape",
            bubbles: true,
          });
          input.dispatchEvent(esc);
        }}
      >
        Esc'ni taqlid qilish
      </button>
    </>
  );
}

Konsolda:

text
Tozalandi

Ekranda:

text
Esc'ni taqlid qilish

Natija oynasida maydonga yozib, Esc'ni bosing — matn o'chadi. Tugma Esc bosilishini dispatchEvent bilan taqlid qiladi (tekshiruv shu yo'l bilan ishlaydi). Unda bubbles: true borligiga e'tibor bering — «Ichkarida React ham delegatsiya qiladi» bo'limidagi qoida.

Bitta nozik holat — IME (xitoy, yapon, koreys yozuvi kiritish usuli). Bunday klaviaturada so'z tanlash oynasidagi Enter "saqla" degani emas — u faqat so'zni tanlaydi. Brauzer buni event.isComposing bilan bildiradi. React'da u event.nativeEvent.isComposing da. vazifalar dagi tahrir maydoni aynan shunday tekshiradi — «Vazifalar qadami» bo'limida.

6. Nomlash va ishlovchini uzatish

React'da ikki qoida keng tarqalgan:

Nima Qoida Misol
hodisa prop'i (komponentning "kirishi") on + Hodisa onRemove, onOrder, onSave
ishlovchi funksiya (ichki) handle + Hodisa handleRemove, handleSubmit

Ota bolaga onRemove={handleRemove} deb beradi: "o'chirish hodisasi bo'lganda — mening handleRemove imni chaqir". vazifalar da nomlar o'zbekcha: prop — onAlmashtir, onOchir. Ishlovchi esa kursdagi funksiya qoidasi bilan — fe'l: katakniAlmashtir, ochirishniSorab.

O'z komponentingizda hodisa prop'ini xohlagancha nomlash mumkin. <button> kabi brauzer elementlarida esa faqat React biladigan nomlar ishlaydi: onClick, onChange, onKeyDown, onSubmit, onFocus, onPointerDown va boshqalar. <button onTap={…}> — xato emas, lekin hech narsa qilmaydi. React konsolda ogohlantiradi: Unknown event handler property `onTap`. It will be ignored. — "noma'lum hodisa xususiyati, e'tiborsiz qoldiriladi".

7. TypeScript: hodisa turlari

Ishlovchi alohida funksiya bo'lsa, unga tur kerak. React o'z hodisa turlarini react paketidan beradi:

tsx
import type { ChangeEvent, FormEvent, KeyboardEvent } from "react";

export default function App() {
  function handleChange(event: ChangeEvent<HTMLInputElement>) {
    console.log("yozildi:", event.target.value);
  }
  function handleKeyDown(event: KeyboardEvent<HTMLInputElement>) {
    if (event.key === "Enter" && !event.nativeEvent.isComposing) {
      console.log("Enter");
    }
  }
  function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
  }
  return (
    <form onSubmit={handleSubmit}>
      <input
        aria-label="Taom"
        onChange={handleChange}
        onKeyDown={handleKeyDown}
      />
    </form>
  );
}

Ekranda:

text
(bo'sh)

<HTMLInputElement> — hodisa qaysi elementda ekani: shuning uchun event.target.value turi string va xatosiz o'qiladi. Eng ko'p uchraydigan xato — turni import qilishni unutish. Unda brauzerning global KeyboardEvent turi olinadi:

tsx
function handleKey(event: KeyboardEvent) {
  console.log(event.key);
}

export function App() {
  return <input onKeyDown={handleKey} />;
}

Xato:

text
TS2322: Type '(event: KeyboardEvent) => void' is not assignable to type 'KeyboardEventHandler<HTMLInputElement>'.
  Types of parameters 'event' and 'event' are incompatible.
    Type 'KeyboardEvent<HTMLInputElement>' is missing the following properties from type 'KeyboardEvent': isComposing, initKeyboardEvent, DOM_KEY_LOCATION_STANDARD, DOM_KEY_LOCATION_LEFT, and 14 more.

"React'ning KeyboardEvent<HTMLInputElement> turida brauzer KeyboardEvent idagi isComposing va yana 17 ta xususiyat yo'q." Ikki xil tur bir xil nomli — biri React'niki, biri brauzerniki. Tuzatish: import type { KeyboardEvent } from "react". Xabarning o'zi isComposing nega event.nativeEvent da ekanini ham ko'rsatdi: React o'ramida u yo'q. Hodisa turlarining to'liq ro'yxati — React + TS III: hodisalar darsida.

8. Vazifalar qadami: qator hodisalari React'da

Ro'yxatni React chizyapti (Ro'yxatlar va key), lekin bosishni hali vanilla delegatsiya ushlaydi. Bugun uni olib tashlaymiz. Kanon fayllaridagi uzun izoh qatorlari telefon uchun qayta bo'lingan. Yangi branch:

bash
git switch -c refactor/qator-hodisalari

8.1 Hodisalar shartnomasi — QatorAmallari

Avval qatorning "kirishlari" — qaysi hodisalar bo'lishi. src/turlar.ts oxiriga:

ts
/**
 * Ro'yxat qatoridagi hodisalar (17/#11). Ota komponent beradi, qator
 * chaqiradi: qator "nima bo'ldi"ni aytadi, "nima qilish"ni ota hal
 * qiladi. Nomlash: prop — onX, ishlovchi — fe'l
 * (katakniAlmashtir)
 */
export type QatorAmallari = {
  readonly onAlmashtir: (id: number) => void;
  readonly onOchir: (id: number) => void;
  readonly onTahrirla: (id: number) => void;
  readonly onSaqla: () => void;
  readonly onBekor: () => void;
};

Beshta hodisa — v6 dagi switch ning beshta case i. Izohdagi qoida — «Nomlash va ishlovchini uzatish» bo'limidagi jadvalning o'zi.

8.2 Tugma va qator

AmalTugmasi ga onClick prop'i qo'shiladi:

diff
 type Props = {
   amal: Amal;
   matn: string; // tugmada ko'rinadigan so'z
   nomi: string; // vazifa matni
+  onClick: () => void;
 };

-export function AmalTugmasi({ amal, matn, nomi }: Props) {
+export function AmalTugmasi({ amal, matn, nomi, onClick }: Props) {
   return (
-    // data-amal — vanilla hodisa tinglovchisi shunga qaraydi
-    // (#11 gacha)
+    // onClick={onClick} — funksiyaning O'ZI uzatiladi;
+    // onClick={onClick()} bo'lsa, u render paytida chaqirilardi
+    // (17/#11). data-amal — e2e ssenariy va fokus shunga qaraydi
+    // (render.tsx)
     <button
       type="button"
       data-amal={amal}
       aria-label={`${matn}: ${nomi}`}
+      onClick={onClick}
     >

OddiyQator — katakda readOnly o'rniga onChange, tugmalarda arrow bilan id (Shartli render darsida va'da qilingan almashinuv):

diff
       <input
         type="checkbox"
         id={katakId}
         data-amal="almashtir"
         checked={vazifa.bajarildi}
-        readOnly
+        onChange={() => onAlmashtir(vazifa.id)}
       />
 …
       <AmalTugmasi
         amal="tahrirla"
         matn="Tahrirlash"
         nomi={vazifa.matn}
+        onClick={() => onTahrirla(vazifa.id)}
+      />

Props turi esa QatorAmallari dan faqat keraklisini oladi: Pick<QatorAmallari, "onAlmashtir" | "onOchir" | "onTahrirla"> (Utility types).

Katakdagi o'zgarish muhim. checked holatdan keladi — bu controlled katak. Bosilganda React avval katakni o'z joyiga qaytaradi va faqat holat o'zgargach yangisini chizadi. Buni jonli ko'ring:

jsx
function App() {
  return (
    <label>
      <input
        type="checkbox"
        checked={false}
        onChange={(event) => {
          console.log("onChange ichida:", event.target.checked);
          setTimeout(() => {
            console.log("keyin:", event.target.checked);
          }, 0);
        }}
      />
      Non olish
    </label>
  );
}

Konsolda:

text
onChange ichida: true
keyin: false

Ekranda:

text
Non olish

Brauzer katakni belgiladi (true), React esa holatda false turganini ko'rib, uni qaytardi. Bu vazifalar uchun foyda. Server rad etsa yoki internet bo'lmasa, holat o'zgarmaydi va katak o'zi eski holatida qoladi. v6 da buning uchun fondaChiz bilan qo'lda qaytarish kerak edi.

8.3 Tahrir qatori: Enter va Esc

TahrirQatori dagi klaviatura — v6 dagi royxat.addEventListener("keydown", …) ning o'rnida:

tsx
export function TahrirQatori({ vazifa, onSaqla, onBekor }: Props) {
  // Enter — saqlash, Esc — bekor. React hodisasi — SyntheticEvent:
  // brauzer hodisasining o'rami, asli — event.nativeEvent. IME
  // (xitoy, yapon yozuvi) tanlov oynasidagi Enter — saqlash emas
  // (isComposing)
  function tugmaBosildi(event: KeyboardEvent<HTMLInputElement>) {
    if (event.nativeEvent.isComposing) {
      return;
    }
    if (event.key === "Enter") {
      event.preventDefault();
      onSaqla();
    } else if (event.key === "Escape") {
      onBekor();
    }
  }

va maydonda — onKeyDown={tugmaBosildi}, tugmalarda — onClick={onSaqla}, onClick={onBekor}. Bu yerda arrow kerak emas: onSaqla argumentsiz, funksiyaning o'zi uzatiladi. KeyboardEvent — «TypeScript: hodisa turlari» bo'limidagidek, react dan import qilingan.

8.4 Ota beradi: {...amallar}

VazifaRoyxati va VazifaQatori beshta funksiyani bitta amallar obyektida tashiydi. VazifaQatori ularni spread bilan qatorga yoyadi (Props dagi spread):

tsx
  // {...amallar} — obyektdagi har bir funksiya alohida prop bo'lib
  // o'tadi; qator faqat o'ziga keraklisini oladi (17/#06: spread)
  if (tahrirda) {
    return <TahrirQatori vazifa={vazifa} {...amallar} />;
  }
  return <OddiyQator vazifa={vazifa} sorov={sorov} {...amallar} />;

Bu yerda spread xavfsiz: amallar DOM elementiga emas, komponentga tushadi. OddiyQator faqat o'zining uchtasini destructuring bilan oladi, onSaqla va onBekor e'tiborsiz qoladi.

8.5 asosiy.ts — delegatsiya o'rniga ulash

Eng katta o'zgarish — v6 dagi ikki tinglovchi (52 qator) o'chadi:

diff
-royxat.addEventListener("click", (event) => {
-  …
-  const nishon = event.target.closest<HTMLElement>("[data-amal]");
-  …
-  switch (amal) {
-    case "almashtir":
-      katakniAlmashtir(id, zaxira);
-      break;
-    …
-  }
-});
-
-royxat.addEventListener("keydown", (event) => { … });
+// Ro'yxat qatorlari — React komponentlari (17/#11): hodisani qator
+// o'zi ushlaydi va shu funksiyalarni chaqiradi. Avval bitta <ul> da
+// click va keydown tinglovchisi data-amal ga qarab tanlardi
+// (delegatsiya)
+qatorAmallariniUla({
+  onAlmashtir(id) {
+    elonQil("");
+    holat.tahrirId = null;
+    katakniAlmashtir(id, qoshniId(id));
+  },
+  …
+  onSaqla: tahrirniSaqla,
+  onBekor: tahrirniBekorQil,
+});

qatorAmallariniUla — render.tsx dagi yangi funksiya: u obyektni saqlab qo'yadi va render() uni <VazifaRoyxati amallar={…}> ga beradi. Holat hali vanilla do'konda — shuning uchun ishlovchilar asosiy.ts da qoldi. State'ni ko'tarish darsida ular App komponentiga ko'chadi.

8.6 knip: keraksiz eksportlar

Delegatsiya olib tashlangach, uchta eksportni endi hech kim ishlatmaydi. npm run knip buni topdi (knip 6.40.0):

text
Unused exports (2)
TAHRIR_MAYDONI_ID  src/render.tsx:8:10
royxat             src/render.tsx:47:14
Unused exported types (1)
Amal  type  src/AmalTugmasi.tsx:4:13

"Ishlatilmaydigan eksportlar." royxat (<ul>) va TAHRIR_MAYDONI_ID ni vanilla tinglovchi ishlatardi, Amal turini — render.ts. Uchalasidan export olib tashlandi. knip darsida o'rnatgan vosita birinchi marta React kodida ish berdi. Tuzatilgandan keyin npm run knip jim tugaydi.

8.7 Testlar: vi.fn() va fireEvent

Test fayliga uchta yangi test qo'shildi. Ulardagi yangi vosita — vi.fn(), "josus" funksiya: necha marta va qaysi argument bilan chaqirilganini eslab qoladi (Vitest II: mock va spy darsida chuqur). fireEvent.click — elementda hodisa chaqiradi:

tsx
test("hodisalar: katak, tahrirlash va o'chirish — vazifa id bilan",
  () => {
  const amallar = amallarYasa(); // beshta vi.fn()
  chiz("", null, amallar);
  fireEvent.click(
    screen.getByRole("checkbox", { name: "Non olish #bozor" }),
  );
  …
  expect(amallar.onAlmashtir).toHaveBeenCalledWith(1);
  expect(amallar.onTahrirla).toHaveBeenCalledWith(2);
  expect(amallar.onOchir).toHaveBeenCalledWith(1);
});

Qolgan ikkitasi: "controlled katak: bosilsa ham holat o'zgarmaguncha eski qiymat" (yuqoridagi jonli misolning testi) va "Enter — saqlash, Esc — bekor, IME tanlovidagi Enter — yo'q". npx vitest run --reporter=verbose (oxirgi uch qator):

text
 ✓ tekshiruv/VazifaRoyxati.test.tsx > VazifaRoyxati > hodisalar: katak, tahrirlash va o'chirish — vazifa id bilan 22ms
 ✓ tekshiruv/VazifaRoyxati.test.tsx > VazifaRoyxati > controlled katak: bosilsa ham holat o'zgarmaguncha eski qiymat 8ms
 ✓ tekshiruv/VazifaRoyxati.test.tsx > VazifaRoyxati > tahrir: Enter — saqlash, Esc — bekor, IME tanlovidagi Enter — yo'q 8ms
      Tests  8 passed (8)

node:test — 146/146, npm run lint, format:check, tip, knip — toza. Brauzer tekshiruvi — 159/159, birinchi urinishda. Ssenariy React hodisalari haqida hech narsa bilmaydi: u haqiqiy bosish qiladi, React esa uni ildizda ushlaydi.

text
refactor: qator hodisalari React'da — delegatsiya o'rniga onClick

Diff: 9 fayl, +208 −86.

9. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
ishlovchi chizishda ishlaydi, bosishda yo'q onClick={f()} onClick={f} yoki () => f(x)
TS2322 … 'void' is not assignable to … EventHandler o'sha o'sha
onclick ishlamaydi kichik harf onClick
sahifa qayta yuklanadi preventDefault() yo'q yoki return false event.preventDefault()
ichki tugma tashqi kartani ham ochadi bubble event.stopPropagation()
onChange har harfda chaqiriladi React'da bu input hodisasi kutilgan xulq
KeyboardEvent … missing … isComposing brauzer turi ishlatilgan import type { KeyboardEvent } from "react"
sun'iy hodisani React ko'rmaydi dispatchEvent da bubbles yo'q { bubbles: true }

10. Mashqlar

Mashqlar kurs/mashqlar/17/11-hodisalar/ da. Har yechimni «Ishga tushir» bilan sinang.

1-mashq (oson): qachon ishlaydi?

Blok tugmalarni ketma-ket bosadi: "A", "B", "C", "D". Ishga tushirmasdan oldin konsolni yozing.

jsx
function log(text) {
  console.log(String(text));
}

function App() {
  return (
    <div onClick={() => log("div")}>
      <button onClick={() => log("A")}>A</button>
      <button onClick={log("B")}>B</button>
      <button onClick={log}>C</button>
      <button
        onClick={(event) => {
          event.stopPropagation();
          log("D");
        }}
      >
        D
      </button>
    </div>
  );
}

Konsolda:

text
B
A
div
div
[object Object]
div
D

Konsolning birinchi qatori — chizish paytida chiqqan:

Yechim
  • B — birinchi qator: log("B") chizishda chaqirildi, onClick ga undefined tushdi. Bosilganda tugmada ishlovchi yo'q, faqat div'ning onClick i ishlaydi (2-"div").
  • A — A, keyin bubble: div.
  • C — onClick={log}: funksiyaning o'zi uzatildi, React uni hodisa obyekti bilan chaqirdi — log(event). Konsolda obyekt satrga aylandi: [object Object]. Keyin div. Ishlovchi har doim hodisani birinchi argument qilib oladi — buni unutmang.
  • D — stopPropagation: faqat D, div yo'q.

2-mashq (o'rta): buyurtma tugmalari

«Bahor» buyurtma ro'yxati: har qatorda taom, soni va uchta tugma: "+", "−", "×" (o'chirish). Hozircha ma'lumot vanilla o'zgaruvchida, chizish — draw() (JSX II dagidek). Yozing:

  • OrderRow komponenti — props: item, onAdd, onRemoveOne, onDelete; tugmalarning aria-label i: "Qo'sh: Osh", "Ayir: Osh", "O'chir: Osh";
  • "−" soni 1 bo'lsa disabled;
  • ishlovchilar App da (vanilla draw() bilan), argument — item.id.

Blok ketma-ket bosadi: oshni ko'paytirish, mantini o'chirish, oshni kamaytirish. Natija:

text
Osh × 1 +−×
Ko'k choy × 2 +−×
Yechim
jsx
import { createRoot } from "react-dom/client";

const root = createRoot(document.getElementById("root"));
let order = [
  { id: 1, dish: "Osh", count: 1 },
  { id: 2, dish: "Manti", count: 3 },
  { id: 3, dish: "Ko'k choy", count: 2 },
];

function OrderRow({ item, onAdd, onRemoveOne, onDelete }) {
  return (
    <li>
      {item.dish} × {item.count}{" "}
      <button
        aria-label={`Qo'sh: ${item.dish}`}
        onClick={() => onAdd(item.id)}
      >
        +
      </button>
      <button
        aria-label={`Ayir: ${item.dish}`}
        disabled={item.count === 1}
        onClick={() => onRemoveOne(item.id)}
      >
        −
      </button>
      <button
        aria-label={`O'chir: ${item.dish}`}
        onClick={() => onDelete(item.id)}
      >
        ×
      </button>
    </li>
  );
}

function change(id, delta) {
  order = order.map((item) =>
    item.id === id ? { ...item, count: item.count + delta } : item
  );
  draw();
}

function handleDelete(id) {
  order = order.filter((item) => item.id !== id);
  draw();
}

function draw() {
  root.render(
    <ul>
      {order.map((item) => (
        <OrderRow
          key={item.id}
          item={item}
          onAdd={(id) => change(id, 1)}
          onRemoveOne={(id) => change(id, -1)}
          onDelete={handleDelete}
        />
      ))}
    </ul>
  );
}

draw();

Ekranda:

text
Osh × 1 +−×
Ko'k choy × 2 +−×

onDelete={handleDelete} — arrow kerak emas: OrderRow uni o'zi id bilan chaqiradi. onAdd va onRemoveOne esa bitta change funksiyasini turli delta bilan o'raydi. order.map(… { ...item, count }) — massivni o'zgartirmasdan yangisini yasash. Bu State III darsining asosiy usuli, hozir u yerdan bir parcha.

3-mashq (qiyin): kartochka va klaviatura (TypeScript)

.tsx da DishCard yozing:

  • butun karta (<article>) bosilsa — onOpen() chaqirilsin;
  • ichidagi "Savatga" tugmasi — faqat onAddToCart(), karta ochilmasin;
  • karta klaviaturadan ham ochilsin: <article tabIndex={0}> va onKeyDown da Enter → onOpen(). Lekin fokus ichidagi tugmada bo'lsa, Enter faqat tugmani bossin — kartani ochmasin. Ishora: event.target === event.currentTarget — hodisa kartaning o'zidami?
  • turlar: MouseEvent, KeyboardEvent — react dan.

App ishlovchilarga console.log beradi. Blok: kartani bosadi, keyin "Savatga" ni bosadi.

Yechim
tsx
import type { KeyboardEvent, MouseEvent } from "react";

type DishCardProps = {
  name: string;
  price: number;
  onOpen: () => void;
  onAddToCart: () => void;
};

function DishCard(props: DishCardProps) {
  const { name, price, onOpen, onAddToCart } = props;
  function handleKeyDown(event: KeyboardEvent<HTMLElement>) {
    const onCard = event.target === event.currentTarget;
    if (event.key === "Enter" && onCard) {
      onOpen();
    }
  }
  function handleAdd(event: MouseEvent<HTMLButtonElement>) {
    event.stopPropagation();
    onAddToCart();
  }
  return (
    <article tabIndex={0} onClick={onOpen} onKeyDown={handleKeyDown}>
      <h3>{name}</h3>
      <p>{price} so'm</p>
      <button type="button" onClick={handleAdd}>
        Savatga
      </button>
    </article>
  );
}

export default function App() {
  return (
    <DishCard
      name="Osh"
      price={35000}
      onOpen={() => console.log("Karta ochildi")}
      onAddToCart={() => console.log("Savatga qo'shildi")}
    />
  );
}

Konsolda:

text
Karta ochildi
Savatga qo'shildi

Ekranda:

text
Osh
35000 so'm
Savatga

Ikkinchi bosishda "Karta ochildi" yo'q — stopPropagation. Klaviatura uchun event.target === event.currentTarget tekshiruvi kerak: tugmada bosilgan Enter ham keydown sifatida kartaga ko'tariladi. Shart bo'lmasa, Bobur tugmada Enter bossa, karta ham ochilardi. onClick={onOpen} — arrow emas: onOpen hodisa obyektini oladi, lekin argument kutmagani uchun uni e'tiborsiz qoldiradi. Turi () => void — TypeScript bunga ruxsat beradi.

4-mashq: Vazifalar qadami

refactor/qator-hodisalari branch'ida «Vazifalar qadami» bo'limidagi o'zgarishlarni kiriting: QatorAmallari, AmalTugmasi, OddiyQator, TahrirQatori, VazifaQatori, VazifaRoyxati, render.tsx (qatorAmallariniUla), asosiy.ts va uchta yangi test. npm run check va npm run knip toza bo'lsin.

Yechim

Tekshiruv ro'yxati:

  • npm test — Vitest 8/8. "controlled katak" testi yiqilsa — checked o'rniga defaultChecked yozilgan bo'lishi mumkin.
  • npm run knip — uchta keraksiz eksport (royxat, TAHRIR_MAYDONI_ID, Amal) tozalanganmi?
  • asosiy.ts da royxat.addEventListener qolmaganmi? Qolsa — har bosishda ish ikki marta bajariladi: React ham, eski tinglovchi ham.
  • Brauzerda: katakni belgilang, tahrirlang, Enter va Esc bilan sinang, o'chiring. Internetni o'chirib katakni bosing — katak o'zi qaytishi kerak (controlled).

Yakuniy kod — refactor: qator hodisalari React'da — delegatsiya o'rniga onClick commit'i.

11. Real ishda

  • Har interfeys — hodisalar to'plami. Kod ko'rigida ko'p uchraydigan izohlar: "onClick={handle()} — qavslarni olib tashlang", "ichki tugmada stopPropagation yo'q".
  • Analitika (Google Analytics, Yandex Metrika) ko'pincha hujjatda yoki ildizda bubble bosqichida tinglaydi. Keraksiz stopPropagation ularni "ko'r" qiladi — shuning uchun undan faqat haqiqatan kerak joyda foydalaning.
  • Testlar hodisalarni fireEvent yoki user-event bilan chaqiradi (RTL II). Haqiqiy foydalanuvchiga yaqinroq — user-event.
  • Intervyu savollari: "onClick={f} va onClick={f()} farqi?", "SyntheticEvent nima?", "React'da onChange qachon ishlaydi?", "React hodisalarni qayerga ulaydi?".

Xulosa

  • Ishlovchi — JSX atributi: onClick={handleClick}; funksiyaning o'zi uzatiladi, chaqiruvi emas.
  • Argument kerak bo'lsa — arrow: onClick={() => onRemove(dish.id)}.
  • Ishlovchi SyntheticEvent oladi: preventDefault, stopPropagation, target, currentTarget; asli — nativeEvent.
  • React onChange — har harfda (brauzerning input hodisasi); onSubmit — formada, preventDefault bilan.
  • Bubble va capture brauzerdagidek; React hodisalarni ildizda ushlaydi — sun'iy hodisaga bubbles: true kerak.
  • vazifalar da delegatsiya o'rniga onClick/onChange/onKeyDown: QatorAmallari, controlled katak, Vitest 8/8, 159/159.

Keyingi dars: State I: useState — komponent xotirasi — ishlovchi ma'lumotni o'zgartirganda ekran o'zi yangilanishi uchun komponentga xotira beramiz.

Manbalar

  • React: "Responding to Events" — react.dev/learn/responding-to-events
  • React: "Common components" — React event object, onChange — react.dev/reference/react-dom/components/common
  • React 17 blogi: "Changes to Event Delegation" (2020-08-10) — legacy.reactjs.org/blog/2020/08/10/react-v17-rc.html
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Hodisalarni boshqarish: React'da onClick, onChange va onSubmit — funksiyani uzating, chaqirmang — IlmHamroh