IlmHamroh
JavaScript Full-stack/17-qism. React asoslari9/45-dars24 daqiqa
Mundarija (31)

Ro'yxatlarni render qilish va key: map, filter va nega index key xato beradi

Qisqacha: React'da ro'yxat massivdan chiziladi: {dishes.map((d) => <MenuItem key={d.id} {...d} />)}. Har elementga key kerak — u React'ga "bu qaysi taom" ekanini aytadi. Ro'yxat o'zgarganda React elementlarni tartib raqami bo'yicha emas, key bo'yicha taniydi. key qardoshlar orasida noyob va barqaror bo'lsin — ma'lumotning o'z id'si. Massiv indeksini key qilsangiz, o'chirish yoki saralashda yozilgan matn boshqa qatorga "ko'chib" qoladi. Renderda crypto.randomUUID() esa har chizishda hamma qatorni yangidan yasaydi.

Bu darsda

  • Massivni map bilan JSX ro'yxatiga, filter + map bilan saralangan ro'yxatga aylantirasiz.
  • key ni qayerga va qanday qiymat bilan qo'yishni bilasiz.
  • Index key qanday xatoga olib kelishini jonli ko'rib, uni tushuntira olasiz.
  • Renderda tasodifiy key yasash nega xavfli ekanini bilasiz.
  • vazifalar ro'yxatini React'ga berasiz va birinchi komponent testlarini ishga tushirasiz.

Oldin bilishingiz kerak: Shartli render, map: har elementni o'zgartirish, filter: shart bo'yicha tanlash, JSX II: ifodalar, Fragment va nima ekranga chiqadi.

1. Nega bu kerak?

Haqiqiy ilovada deyarli hamma narsa ro'yxat: menyu, buyurtmalar, izohlar, vazifalar. Ma'lumot serverdan massiv bo'lib keladi. Taomlar soni bugun 12 ta, ertaga 15 ta — JSX'da qo'lda yozib bo'lmaydi.

vazifalar v6 da ro'yxat mana bunday chizilardi:

ts
royxat.replaceChildren(...korinadiganlar().map(qatorYasa));

Har o'zgarishda hamma <li> o'chirilib, qaytadan yasalardi. Bitta katakni belgilasangiz ham — 1 000 qator noldan. React nima va nega darsida o'lchagan raqamni eslang: 1 000 vazifada bitta katak 47–77 ms. React esa faqat o'zgargan qatorga tegadi — 19–20 ms. Lekin buning uchun React har qatorni tanib olishi kerak: "eski ro'yxatdagi 2-qator yangi ro'yxatda qayerda?". Bugungi darsning yuragi — shu tanib olish, ya'ni key.

Hayotiy o'xshatish: kinoteatrdagi chiptalar. O'rindiqlar raqamlangan, chiptada raqam yozilgan. Kimdir chiqib ketsa ham, qolganlar o'z o'rindig'ida o'tiradi. Agar odamni "qatorda nechanchi turgani" bo'yicha tanisangiz edi, bitta odam chiqqanda hamma bir o'rin surilardi — va hamma birovning o'rnida o'tirib qolardi.

2. map bilan ro'yxat

2.1 Massivdan elementlar massivi

JSX II darsida ko'rgandik: massiv {} ichida chizilsa, elementlari ketma-ket chiqadi. map esa ma'lumot massivini element massiviga aylantiradi:

jsx
const menu = [
  { id: 1, name: "Osh", price: 35000 },
  { id: 2, name: "Lag'mon", price: 28000 },
  { id: 3, name: "Manti", price: 30000 },
];

function MenuItem({ name, price }) {
  return (
    <li>
      {name} — {price} so'm
    </li>
  );
}

function App() {
  return (
    <ul>
      {menu.map((dish) => (
        <MenuItem key={dish.id} name={dish.name} price={dish.price} />
      ))}
    </ul>
  );
}

Ekranda:

text
Osh — 35000 so'm
Lag'mon — 28000 so'm
Manti — 30000 so'm

Bosqichma-bosqich: menu.map(...) uchta <MenuItem …/> elementini qaytaradi → [el1, el2, el3] massivi <ul> ichiga tushadi → React ularni ketma-ket chizadi. {...dish} spread (Props) bilan qisqaroq ham yoziladi: <MenuItem key={dish.id} {...dish} />.

2.2 key — map dagi eng tashqi elementga

Diqqat qiling: key <li> ga emas, <MenuItem> ga qo'yildi. Qoida: key — map qaytaradigan eng tashqi elementda. React qardoshlarni solishtiradi — bu yerda qardoshlar MenuItem lar. <li> esa har MenuItem ning ichida yolg'iz.

Ko'p uchraydigan xato — key ni komponent ichiga qo'yish:

jsx
function MenuItem({ name }) {
  return <li key={name}>{name}</li>; // ❌ bu yerda foydasiz
}

function App() {
  const dishes = ["Osh", "Manti"];
  return (
    <ul>
      {dishes.map((d) => (
        <MenuItem name={d} />
      ))}
    </ul>
  );
}

Konsolda:

text
Each child in a list should have a unique "key" prop.
Check the render method of `App`. See https://react.dev/link/warning-keys for more information.

Ekranda:

text
Osh
Manti

"Ro'yxatdagi har bolada noyob key bo'lishi kerak. App ning render metodini tekshiring." React to'g'ri joyni ko'rsatdi: muammo App da, map qatorida. MenuItem ichidagi key — boshqa ro'yxatda, u yerda qardosh yo'q.

2.3 filter + map

Ro'yxatning bir qismi kerak bo'lsa — avval filter, keyin map (metod zanjiri):

jsx
const menu = [
  { id: 1, name: "Osh", price: 35000, soldOut: false },
  { id: 2, name: "Somsa", price: 8000, soldOut: true },
  { id: 3, name: "Manti", price: 30000, soldOut: false },
  { id: 4, name: "Ko'k choy", price: 5000, soldOut: false },
];

function App() {
  const available = menu.filter((dish) => !dish.soldOut);
  const cheapFirst = available.toSorted((a, b) => a.price - b.price);
  return (
    <>
      <p>Bugun {available.length} ta taom bor:</p>
      <ol>
        {cheapFirst.map((dish) => (
          <li key={dish.id}>
            {dish.name} — {dish.price} so'm
          </li>
        ))}
      </ol>
    </>
  );
}

Ekranda:

text
Bugun 3 ta taom bor:
Ko'k choy — 5000 so'm
Manti — 30000 so'm
Osh — 35000 so'm

Ikki narsa muhim. Birinchisi: hisob-kitob (filter, toSorted) return dan oldin, oddiy o'zgaruvchilarda. JSX'da faqat chizish qoldi — o'qish oson. Ikkinchisi: sort emas, toSorted (Nusxa bilan o'zgartirish). sort asl massivni joyida o'zgartiradi — menu ni ham. Komponent tashqaridagi ma'lumotni buzmasligi kerak (Pure komponentlar darsida nega). id lar tartib o'zgarsa ham taomga "yopishib" qoldi: Ko'k choy — key={4}, qayerda turmasin.

Ro'yxat bo'sh bo'lishi mumkinligini unutmang: Shartli render darsidagi "bo'sh holat" — available.length === 0 bo'lsa, <ol> o'rniga xabar.

Tekshirib ko'ring: {menu.map((dish) => { <li key={dish.id}>{dish.name}</li> })} — ekranda nima chiqadi?

Javob

Hech narsa. Jingalak qavsli arrow funksiya return siz undefined qaytaradi — map [undefined, undefined, …] massivini beradi, React esa undefined ni chizmaydi. Komponentlar darsidagi return unutilgan xatoning o'zi. Tuzatish: oddiy qavs (dish) => (<li …>…</li>) yoki { return <li …/>; }.

3. key qoidalari

3.1 Qanday qiymat key bo'ladi

key — satr yoki son. Uch talab:

Talab Ma'nosi Buzilsa
Noyob qardoshlar orasida takrorlanmaydi ogohlantirish, qator yo'qolishi
Barqaror bir element uchun har chizishda bir xil qator har safar qayta yasaladi
Ma'lumotdan elementning o'zi bilan bog'liq (id) o'chirishda qiymatlar "ko'chadi"

Eng yaxshi key — ma'lumotning o'z id'si: bazadagi id, vazifalar dagi vazifa.id, mahsulot kodi. Noyoblik faqat qardoshlar orasida kerak: ikki xil ro'yxatda bir xil key bo'lishi mumkin.

3.2 Takroriy key

Ikki taomga xato bilan bir xil id berilgan:

jsx
function App() {
  const dishes = [
    { id: 1, name: "Osh" },
    { id: 1, name: "Manti" },
  ];
  return (
    <ul>
      {dishes.map((d) => (
        <li key={d.id}>{d.name}</li>
      ))}
    </ul>
  );
}

Konsolda:

text
Encountered two children with the same key, `1`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.

Ekranda:

text
Osh
Manti

"Bir xil 1 kalitli ikki bola topildi. Kalitlar noyob bo'lishi kerak — shunda komponentlar yangilanishlar orasida o'zligini saqlaydi. Noyob bo'lmagan kalitlar bolalarni takrorlashi yoki tushirib qoldirishi mumkin." Hozir ekran to'g'ri, lekin bu tasodif: ro'yxat o'zgarganda bitta qator ikki marta chiqishi yoki yo'qolishi mumkin. Odatda bunday xato ma'lumotning o'zida — server ikki xil yozuvga bir id bergan. Uni key'ni "boshqacha qilib" emas, ma'lumot manbasida tuzatish kerak.

3.3 key props'ga kirmaydi

Props darsida ko'rgandik: key komponentga yetib bormaydi, props.key — undefined va ogohlantirish. Id komponentning ichida ham kerak bo'lsa — ikki marta bering: <MenuItem key={dish.id} id={dish.id} />. Bu takror emas: birinchisi React uchun, ikkinchisi sizning kodingiz uchun.

4. Index key nega xato beradi

4.1 Tuzoqni jonli ko'ramiz

Eng ko'p uchraydigan "yechim": "id yo'q ekan — indeksni beraman". map ning ikkinchi argumenti — index (Callback imzosi). Ogohlantirish yo'qoladi, lekin xato qoladi. Ko'ramiz: har taom yonida izoh maydoni bor ("piyozsiz", "achchiq"). Blok "Osh izohi" ga "piyozsiz" deb yozadi, keyin oshni ro'yxatdan o'chiradi:

jsx
import { flushSync } from "react-dom";
import { createRoot } from "react-dom/client";

const root = createRoot(document.getElementById("root"));
let dishes = [
  { id: 1, name: "Osh" },
  { id: 2, name: "Manti" },
  { id: 3, name: "Lag'mon" },
];

function DishRow({ name }) {
  return (
    <li>
      {name}: <input aria-label={`${name} izohi`} />
    </li>
  );
}

function draw() {
  root.render(
    <>
      <ul>
        {dishes.map((dish, index) => (
          <DishRow key={index} name={dish.name} /> // ❌ index
        ))}
      </ul>
      <button onClick={removeFirst}>Oshni o'chirish</button>
    </>
  );
}

function removeFirst() {
  dishes = dishes.slice(1);
  flushSync(draw);
  for (const input of document.querySelectorAll("input")) {
    const label = input.getAttribute("aria-label");
    console.log(label, JSON.stringify(input.value));
  }
}

draw();

Konsolda:

text
Manti izohi "piyozsiz"
Lag'mon izohi ""

Ekranda:

text
Manti:
Lag'mon:
Oshni o'chirish

Osh o'chdi, lekin uning "piyozsiz" izohi mantiga o'tib qoldi. Oshpaz piyozsiz manti tayyorlaydi — mijoz esa bunday so'ramagan edi. «Ishga tushir» bilan o'zingiz sinang: istalgan izohni yozib, oshni o'chiring.

4.2 Nega shunday bo'ldi

React eski va yangi ro'yxatni key bo'yicha juftlaydi:

key Oldin Keyin React qarori
0 Osh, input: "piyozsiz" Manti "0-element qoldi, nomi o'zgardi" — input'ga tegmaydi
1 Manti, input: "" Lag'mon "1-element qoldi, nomi o'zgardi"
2 Lag'mon, input: "" — "2-element yo'qoldi" — oxirgisini o'chiradi

React nuqtai nazaridan hech kim o'chirilmadi — oxirgisi o'chirildi, qolganlarning matni yangilandi. Matn props'dan keladi va to'g'ri yangilandi. <input> dagi yozuv esa DOM elementining o'zida turadi — u key 0 li elementda qoldi. Endi o'sha elementda "Manti" yozilgan.

Diagrammada: chapda eski ro'yxat, o'ngda yangi. Chiziqlar React qaysi elementni qaysi biriga moslaganini ko'rsatadi:

flowchart LR
  subgraph Oldin
    A0["key 0: Osh + piyozsiz"]
    A1["key 1: Manti"]
    A2["key 2: Lag'mon"]
  end
  subgraph Keyin
    B0["key 0: Manti + piyozsiz"]
    B1["key 1: Lag'mon"]
  end
  A0 --> B0
  A1 --> B1
  A2 -. "o'chirildi" .-> X["—"]

4.3 Tuzatish — id

Bitta o'zgarish: key={index} o'rniga key={dish.id}:

jsx
import { flushSync } from "react-dom";
import { createRoot } from "react-dom/client";

const root = createRoot(document.getElementById("root"));
let dishes = [
  { id: 1, name: "Osh" },
  { id: 2, name: "Manti" },
  { id: 3, name: "Lag'mon" },
];

function DishRow({ name }) {
  return (
    <li>
      {name}: <input aria-label={`${name} izohi`} />
    </li>
  );
}

function draw() {
  root.render(
    <>
      <ul>
        {dishes.map((dish) => (
          <DishRow key={dish.id} name={dish.name} /> // ✅ id
        ))}
      </ul>
      <button onClick={removeFirst}>Oshni o'chirish</button>
    </>
  );
}

function removeFirst() {
  dishes = dishes.slice(1);
  flushSync(draw);
  for (const input of document.querySelectorAll("input")) {
    const label = input.getAttribute("aria-label");
    console.log(label, JSON.stringify(input.value));
  }
}

draw();

Konsolda:

text
Manti izohi ""
Lag'mon izohi ""

Ekranda:

text
Manti:
Lag'mon:
Oshni o'chirish

Endi React ko'radi: "key 1 (Osh) yo'qoldi, 2 va 3 qoldi". Osh qatori izohi bilan birga o'chdi, qolganlariga tegilmadi. Xuddi o'rindiq raqamli chiptalar kabi.

4.4 Index qachon mumkin

Index key xato har doim emas, faqat ro'yxat o'zgarganda chiqadi. Uchala shart bajarilsa — index ham bo'ladi:

  1. ro'yxat hech qachon tartibi o'zgarmaydi (saralash, o'rtadan qo'shish yo'q);
  2. o'rtadan elementi o'chirilmaydi va filtrlanmaydi;
  3. elementlarda o'z holati yo'q (input, ochiq-yopiqlik, keyinchalik state).

Masalan, qadamlar ro'yxati ("1. Guruchni yuving, 2. …") — statik matn. Lekin "keyin hech qachon o'zgarmaydi" degan va'dani berish qiyin. Odat qiling: id bo'lsa — id. Id yo'q bo'lsa — uni ma'lumot yaratilgan paytda bering (pastda).

Tekshirib ko'ring: Index key bilan ro'yxatning oxiriga yangi taom qo'shilsa, izoh "ko'chadimi"? Boshiga qo'shilsa-chi?

Javob

Oxiriga qo'shilsa — yo'q: eski elementlar o'z indekslarini saqlaydi (0, 1, 2), yangisi 3 oladi. Boshiga qo'shilsa — ha: hamma indeks bittaga suriladi, yangi taom 0 ni oladi va eski oshning izohi unga "ko'chadi". Shuning uchun index key bilan yozilgan kod uzoq vaqt ishlab, birinchi "boshiga qo'shish" yoki saralashda buziladi.

5. Renderda tasodifiy key — crypto.randomUUID()

Index yomon ekanini bilgan dasturchi ba'zan teskarisini qiladi:

jsx
import { flushSync } from "react-dom";
import { createRoot } from "react-dom/client";

const root = createRoot(document.getElementById("root"));
const dishes = [{ id: 1, name: "Osh" }];

function draw() {
  root.render(
    <>
      <ul>
        {dishes.map((dish) => (
          // ❌ har chizishda yangi key
          <li key={crypto.randomUUID()}>
            {dish.name}: <input aria-label={`${dish.name} izohi`} />
          </li>
        ))}
      </ul>
      <button onClick={redraw}>Qayta chizish</button>
    </>
  );
}

function redraw() {
  flushSync(draw);
  const input = document.querySelector("input");
  console.log("Izoh:", JSON.stringify(input.value));
}

draw();

Konsolda:

text
Izoh: ""

Ekranda:

text
Osh:
Qayta chizish

Ro'yxat o'zgarmadi — shunchaki qayta chizildi. Lekin izoh yo'qoldi. crypto.randomUUID() — brauzer va Node'dagi tasodifiy noyob id (UUID) — har chaqiruvda yangi qiymat beradi. Har chizishda key yangi — React "eski element yo'qoldi, yangisi keldi" deb o'ylaydi va har qatorni o'chirib, qaytadan yasaydi. Bu Komponentlar darsidagi "ichma-ich komponent" tuzog'ining aynan o'zi: barqarorlik yo'q.

Bu ikki marta zararli: matn va fokus yo'qoladi, va har chizishda butun ro'yxat qayta quriladi — v6 dagi replaceChildren ga qaytdik. Tuzatish: id'ni ma'lumot yaratilganda bering, chizishda emas:

js
const dishes = [];

function addDish(name) {
  // ✅ id bir marta, qo'shilayotganda beriladi va saqlanadi
  dishes.push({ id: crypto.randomUUID(), name });
}

addDish("Osh");
addDish("Manti");
console.log(dishes.length, dishes[0].id === dishes[0].id); // 2 true
console.log(typeof dishes[1].id); // string

vazifalar da ham shunday: vazifa qo'shilganda id oladi va u localStorage da, serverda — hamma joyda vazifa bilan birga yuradi.

6. key va TypeScript

key ning turi — Key (@types/react dan): string | number | bigint. Shuning uchun obyektni yoki undefined bo'lishi mumkin bo'lgan qiymatni key qilib bo'lmaydi:

tsx
type Dish = { id?: number; name: string };

const dishes: Dish[] = [{ id: 1, name: "Osh" }];

export function App() {
  return (
    <ul>
      {dishes.map((dish) => (
        <li key={dish}>{dish.name}</li>
      ))}
    </ul>
  );
}

Xato:

text
TS2322: Type 'Dish' is not assignable to type 'Key | null | undefined'.

"Dish turini Key | null | undefined ga berib bo'lmaydi." Obyekt kalit bo'la olmaydi — dish.id kerak edi. E'tibor bering: undefined ruxsat etilgan. Ya'ni id?: number bo'lsa ham, key={dish.id} ni TypeScript o'tkazib yuboradi. Ma'lumotda id yo'q bo'lsa, React key'siz ogohlantirish beradi. Shuning uchun ro'yxat elementi turida id majburiy bo'lsin: vazifalar dagi VazifaMalumoti da readonly id: number.

7. Vazifalar qadami: ro'yxatni React chizadi

Besh darslik ishning oxirgi qadami. Komponentlar darsidan beri yozgan fayllar bugun ishga tushadi va commit bo'ladi. Kanon fayllaridagi uzun izoh qatorlari telefon ekraniga sig'ishi uchun qayta bo'lingan — mazmuni o'zgarmagan.

7.1 VazifaRoyxati

src/VazifaRoyxati.tsx:

tsx
// VazifaRoyxati.tsx — ro'yxat qatorlari (17/#09). <ul id="royxat">
// hali index.html da — React uning ICHINI chizadi (render.tsx)
import { VazifaQatori } from "./VazifaQatori.tsx";
import type { VazifaMalumoti } from "./turlar.ts";

type Props = {
  vazifalar: readonly VazifaMalumoti[];
  sorov: string;
  tahrirId: number | null;
};

export function VazifaRoyxati({ vazifalar, sorov, tahrirId }: Props) {
  // key — vazifa id: barqaror va takrorlanmaydi. Tartib o'zgarsa
  // (saralash, bajarilgan pastga tushdi) React <li> ni yangidan
  // yasamaydi, joyini almashtiradi — fokus va yozilgan matn
  // saqlanadi
  return vazifalar.map((vazifa) => (
    <VazifaQatori
      key={vazifa.id}
      vazifa={vazifa}
      sorov={sorov}
      tahrirda={vazifa.id === tahrirId}
    />
  ));
}
  • return vazifalar.map(...) — komponent massiv qaytaradi, <ul> siz. <ul id="royxat"> index.html da qoladi (e2e va CSS unga qaraydi), React esa uning ichini chizadi. Komponent massiv qaytara oladi — Komponentlar darsidagi qoida.
  • key={vazifa.id} — izohda yozilganidek: vazifalar saralanadi, bajarilgani pastga tushadi, filtr o'zgaradi. Hammasida React qatorni id bo'yicha taniydi. Tahrir maydonidagi yozuv "ko'chib" ketmaydi — «Index key nega xato beradi» bo'limidagi xato bu yerda bo'lishi mumkin emas.
  • tahrirda={vazifa.id === tahrirId} — hisoblangan boolean. VazifaQatori global holatni bilmaydi, unga tayyor javob beriladi (Shartli render).
  • readonly VazifaMalumoti[] — komponent ro'yxatni faqat o'qiydi. push yozilsa, tsc to'xtatadi (Massiv va tuple turlari).

7.2 render.ts → render.tsx

Endi vanilla render() ga ro'yxatni React'ga topshirishni o'rgatamiz. Fayl JSX oladi — kengaytmasi .tsx bo'ladi (git mv src/render.ts src/render.tsx). asosiy.ts dagi import ham "./render.tsx" ga o'zgaradi. Asosiy o'zgarishlar:

diff
-// render.ts — holatdan ekranga: chizish, fokus, e'lon
-import { Vazifa } from "./vazifa.ts";
+// render.tsx — holatdan ekranga: chizish, fokus, e'lon. Ro'yxat
+// qatorlarini endi React chizadi (17/#09), qolgani — hali vanilla
+import { StrictMode } from "react";
+import { flushSync } from "react-dom";
+import { createRoot } from "react-dom/client";
+import { VazifaRoyxati } from "./VazifaRoyxati.tsx";
+import { TAHRIR_MAYDONI_ID } from "./TahrirQatori.tsx";
 …
-export const TAHRIR_MAYDONI_ID = "tahrir-maydon";
+export { TAHRIR_MAYDONI_ID };

Keyin 84 qatorlik vanilla kod o'chadi: tugmaYasa va uning to'rtta .bind nusxasi, belgilanganMatn, oddiyQator, tahrirQatori, qatorYasa. Ularning o'rnini olti komponent egalladi. O'rniga uch qator:

tsx
// <ul id="royxat"> ichi — React ildizi (root). Ichini endi faqat
// React o'zgartiradi: replaceChildren yoki append bu yerda
// ishlatilmaydi
const royxatIldizi = createRoot(royxat);

Va render() ning birinchi qatori:

diff
 export function render(): void {
-  royxat.replaceChildren(...korinadiganlar().map(qatorYasa));
+  // root.render() navbatga qo'yadi, DOM keyinroq yangilanadi.
+  // flushSync — hozir, shu qatorda: pastdagi kod (bosh xabar,
+  // qidiruv natijasi, fokus) yangi qatorlarga qaraydi
+  flushSync(() => {
+    royxatIldizi.render(
+      <StrictMode>
+        <VazifaRoyxati
+          vazifalar={korinadiganlar()}
+          sorov={holat.qidiruv}
+          tahrirId={holat.tahrirId}
+        />
+      </StrictMode>,
+    );
+  });
   // Ro'yxatdan keyin: bosh xabar qatorlar soniga qaraydi
   boshXabarniYoz();

Bu — vazifalar dagi ikkinchi React orol (Birinchi React loyiha dagi #izoh birinchisi edi). createRoot bir marta chaqiriladi, royxatIldizi.render(...) esa har holat o'zgarishida. Bu JSX II dagi draw() ning aynan o'zi: holat o'zgardi → yangi JSX → React farqni topadi.

flushSync nega kerak. root.render() chizishni darhol bajarmaydi — navbatga qo'yadi. render() ning pastki qatorlari esa yangi qatorlarga qaraydi. boshXabarniYoz() "ro'yxat bo'sh" xabarini qatorlar soniga qarab chiqaradi, fokus funksiyasi yangi tugmani qidiradi. flushSync — "chizishni hozir, shu qatorda tugat". Bu kam ishlatiladigan vosita; nega u odatda kerak emasligini State II darsida ko'ramiz. Bizda u vaqtinchalik ko'prik — vanilla va React bir ekranda yashayotgan payt uchun.

Holat va hodisalar hali vanilla. Katakni bosish, o'chirish, tahrirlash — asosiy.ts dagi delegatsiya tinglovchisi data-amal ga qarab ushlaydi. React chizgan <button data-amal="ochir"> ham oddiy DOM tugmasi — delegatsiya uni farqlamaydi. Bular Hodisalarni boshqarish darsida React'ga o'tadi.

7.3 Birinchi komponent testlari: Vitest

16-qismda sof mantiq testlari node:test da edi: tekshiruv/*.test.js, build'siz. Komponentni sinash uchun esa DOM kerak, Node'da esa DOM yo'q. Shuning uchun ikkinchi test qavati qo'shiladi. Vitest — Vite asosidagi test vositasi. jsdom — Node ichidagi "brauzer o'rinbosari". Testing Library — komponentni chizib, elementni foydalanuvchi kabi (roli va nomi bilan) topish kutubxonasi. Bularni chuqur Vitest I va React Testing Library I darslarida o'rganamiz. Bugun — faqat ulash va o'qish.

bash
npm i -D vitest@5.0.4 jsdom@30.1.2 \
  @testing-library/react@16.3.3 @testing-library/dom@10.4.2
text
added 62 packages, and audited 327 packages in 19s

98 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

package.json da "test": "node --test && vitest run" — ikki qavat ketma-ket. Yangi vitest.config.ts:

ts
// vitest.config.ts — komponent testlari (17/#09). Sof mantiq
// testlari node:test da qoladi (tekshiruv/*.test.js, build'siz,
// tez); React komponentlari uchun DOM kerak — Vitest + jsdom
// (brauzer o'rinbosari) + Testing Library. vite.config.ts
// ishlatilmaydi: u .env.<mode> ni talab qiladi, testga esa faqat
// JSX kerak
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  test: {
    environment: "jsdom",
    // Faqat .tsx: tekshiruv/*.test.js — node:test fayllari, Vitest
    // ularni o'qisa "node:test" importida yiqiladi
    include: ["tekshiruv/**/*.test.tsx"],
    setupFiles: ["tekshiruv/vitest-sozlash.ts"],
  },
});

Yana uchta kichik fayl: tekshiruv/vitest-sozlash.ts (har testdan keyin chizilganni tozalaydi — afterEach(cleanup)), tsconfig.test.json (test fayllari uchun uchinchi tsconfig) va tsconfig.json dagi references ga shu fayl. Hammasi kanonda izohlari bilan.

Test faylidan bitta misol — tekshiruv/VazifaRoyxati.test.tsx:

tsx
test("foydalanuvchi matni HTML bo'lib qolmaydi", () => {
  const { container } = render(
    <ul>
      <VazifaRoyxati
        vazifalar={[
          {
            id: 7,
            matn: "<img src=x onerror=alert(1)>",
            bajarildi: false,
          },
        ]}
        sorov=""
        tahrirId={null}
      />
    </ul>,
  );
  expect(container.querySelector("img")).toBeNull();
  expect(container.querySelector("label")?.textContent).toBe(
    "<img src=x onerror=alert(1)>",
  );
});

O'qib ko'ring: vazifa matni — XSS urinishi (JSX II dagi "Hujumchi nigohi"). Test ikki narsani tekshiradi: DOM'da <img> yo'q va yorliq matni harfma-harf o'sha satr. Qolgan to'rtta test: har vazifaga katak, yorliq va ikki tugma; bajarilgan qator klassi; qidiruvda <mark>; tahrir qatori.

bash
npm test

Birinchi ishga tushirish sekin — jsdom muhiti yuklanadi (bizda 67 s, Windows'da antivirus ham sekinlashtiradi). Keyingilari 3–5 s. Chiqishning Vitest qismi (--reporter=verbose bilan). Ikki narsaga qarang: har qator boshidagi (test o'tdi) va oxirgi ikki qatordagi sonlar:

text
 ✓ tekshiruv/VazifaRoyxati.test.tsx > VazifaRoyxati > har vazifaga qator: katak, yorliq, ikki tugma 151ms
 ✓ tekshiruv/VazifaRoyxati.test.tsx > VazifaRoyxati > bajarilgan: katak belgilangan, qator klassi 12ms
 ✓ tekshiruv/VazifaRoyxati.test.tsx > VazifaRoyxati > qidiruvga mos bo'lak <mark> ichida, yorliq matni to'liq 14ms
 ✓ tekshiruv/VazifaRoyxati.test.tsx > VazifaRoyxati > tahrirdagi qator: maydon va Saqlash/Bekor qilish 15ms
 ✓ tekshiruv/VazifaRoyxati.test.tsx > VazifaRoyxati > foydalanuvchi matni HTML bo'lib qolmaydi 7ms

 Test Files  1 passed (1)
      Tests  5 passed (5)

node:test qismi o'zgarmadi: ℹ tests 146 · ℹ pass 146. Sof mantiq React'dan bexabar — ro'yxat qanday chizilishi uni qiziqtirmaydi.

7.4 Build, brauzer tekshiruvi va commit

npm run build — kirish chunk'i 245.21 kB (gzip 78.13). Birinchi React loyiha dagi orol bilan 244.82 kB edi. Olti komponent atigi 0.4 kB qo'shdi: React'ning asosiy narxi bir martalik. npm run lint, format:check, tip — toza.

159 ta brauzer tekshiruvi — 159/159. Lekin bu raqamga oson yetilmadi, va bu yerda muhim saboq bor. Birinchi urinishda natija 150/150 chiqdi — hammasi yashil, faqat 9 ta tekshiruv kam. Ssenariy dialog tekshiruvlarini faqat render.ts fayli bor bo'lsa ishga tushirardi. Fayl render.tsx bo'lgach, butun blok jim o'tkazib yuborildi. Ssenariy tuzatildi: endi xususiyat yo'q bo'lsa, tekshiruv yiqiladi. Qoida: "hammasi yashil" bilan birga nechta tekshiruv o'tganiga ham qarang.

bash
git add -A
git commit
text
refactor: ro'yxat qatorlarini React chizadi (props, children, key)

<ul id="royxat"> ichi — React ildizi; holat va hodisalar hali vanilla.
render() — flushSync(root.render). Birinchi komponent testlari:
Vitest 5.0.4 + Testing Library + jsdom.

Diff: 17 fayl, +1250 −95 (lock-fayl bilan). PR — avvalgidek, gh pr merge --merge. Foydalanuvchi uchun hech narsa o'zgarmadi — va maqsad aynan shu edi.

8. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
unique "key" prop map da key yo'q yoki ichki elementda map qaytargan eng tashqi elementga
two children with the same key id takrorlangan ma'lumot manbasini tuzating
matn boshqa qatorga "ko'chdi" key={index} + o'chirish/saralash key={item.id}
har chizishda matn va fokus yo'qoladi renderda crypto.randomUUID() id'ni yaratishda bering
ro'yxat bo'sh, xato yo'q map da { … } va return yo'q (…) => qavs yoki return
asl massiv tartibi buzildi renderda sort() toSorted()

9. Mashqlar

Mashqlar kurs/mashqlar/17/09-royxat-key/ da. Har yechimni «Ishga tushir» bilan sinang.

1-mashq (oson): key'ni toping

Har qator uchun eng yaxshi key ni tanlang. Variantlar: (a) foydalanuvchilar ro'yxati — { id, phone, name }; (b) haftaning yetti kuni — ["Dushanba", …], o'zgarmaydi; (c) buyurtma qatorlari — { dishId, count }, bitta taom bir buyurtmada bir marta; (d) izohlar — { text, author }, id yo'q.

  • (a) uchun:
  • (b) uchun kun nomi yoki indeks — ikkalasi ham bo'ladimi (ha/yo'q):
Yechim

(a) id — telefon ham noyob, lekin u o'zgarishi mumkin (barqaror emas). (b) ro'yxat statik, nomlar noyob — kun nomi ham, indeks ham ishlaydi; nom aniqroq. (c) dishId — bitta buyurtmada takrorlanmaydi. (d) Ishonchli key yo'q: text takrorlanishi mumkin, indeks esa o'chirishda xato beradi. To'g'ri yo'l — izoh yaratilganda id berish (server yoki crypto.randomUUID() qo'shish paytida).

2-mashq (o'rta): menyu bo'limlari

menu massividan ikki darajali ro'yxat chizing: har bo'lim (group) — <h3>, uning ostida <ul> va taomlar. Faqat soldOut: false taomlar chiqsin. Bo'limda taom qolmasa, bo'lim umuman chiqmasin. Har map da to'g'ri key bo'lsin, konsol toza bo'lsin.

jsx
const menu = [
  { id: 1, group: "Issiq taomlar", name: "Osh", soldOut: false },
  { id: 2, group: "Issiq taomlar", name: "Somsa", soldOut: true },
  { id: 3, group: "Ichimliklar", name: "Ko'k choy", soldOut: false },
  { id: 4, group: "Shirinliklar", name: "Chak-chak", soldOut: true },
  { id: 5, group: "Issiq taomlar", name: "Manti", soldOut: false },
];

Ekranda shunday bo'lsin:

text
Issiq taomlar
Osh
Manti
Ichimliklar
Ko'k choy

Ishora: avval filter, keyin bo'limlar ro'yxati: [...new Set(available.map((d) => d.group))] (Set). Har bo'lim uchun yana filter + map. Ikkita map — ikkita key: tashqisida bo'lim nomi (noyob), ichkisida taom id.

Yechim
jsx
const menu = [
  { id: 1, group: "Issiq taomlar", name: "Osh", soldOut: false },
  { id: 2, group: "Issiq taomlar", name: "Somsa", soldOut: true },
  { id: 3, group: "Ichimliklar", name: "Ko'k choy", soldOut: false },
  { id: 4, group: "Shirinliklar", name: "Chak-chak", soldOut: true },
  { id: 5, group: "Issiq taomlar", name: "Manti", soldOut: false },
];

function App() {
  const available = menu.filter((dish) => !dish.soldOut);
  const groups = [...new Set(available.map((d) => d.group))];
  return (
    <div>
      {groups.map((group) => (
        <section key={group}>
          <h3>{group}</h3>
          <ul>
            {available
              .filter((dish) => dish.group === group)
              .map((dish) => (
                <li key={dish.id}>{dish.name}</li>
              ))}
          </ul>
        </section>
      ))}
    </div>
  );
}

Ekranda:

text
Issiq taomlar
Osh
Manti
Ichimliklar
Ko'k choy

"Shirinliklar" chiqmadi: bo'limlar ro'yxati available dan yasalgan, u yerda esa chak-chak yo'q. Bo'sh bo'lim uchun alohida shart kerak bo'lmadi. key={group} — bo'lim nomlari Set dan, demak noyob. Ichki key={dish.id} faqat o'z <ul> idagi qardoshlar orasida noyob bo'lishi yetadi.

3-mashq (qiyin): index key xatosini tuzating (TypeScript)

Sardor buyurtma ro'yxatini yozdi: har qatorda taom nomi va miqdor maydoni (<input type="number">). "Saralash" tugmasi ro'yxatni nom bo'yicha tartiblaydi. Mijoz oshga 3 yozib, "Saralash" ni bosdi — 3 boshqa taomga o'tib ketdi. Kodni .tsx da tuzating. Shartlar: Order turi { id: number; dish: string }; saralash asl massivni o'zgartirmasin (toSorted, localeCompare); key to'g'ri bo'lsin.

tsx
// ❌ Sardorning kodi (qisqartirilgan)
function draw() {
  root.render(
    <>
      <ul>
        {orders.map((o, i) => (
          <li key={i}>
            {o.dish}:{" "}
            <input type="number" aria-label={`${o.dish} soni`} />
          </li>
        ))}
      </ul>
      <button onClick={sortOrders}>Saralash</button>
    </>
  );
}
function sortOrders() {
  orders.sort((a, b) => a.dish.localeCompare(b.dish));
  draw();
}

Tuzatilgan versiyada orders — [{ id: 1, dish: "Osh" }, { id: 2, dish: "Manti" }, { id: 3, dish: "Lag'mon" }]. "Osh soni" ga 3 yozib, "Saralash" bosilganda konsolda har maydon nomi va qiymati chiqsin — 3 oshda qolsin.

Yechim
tsx
import { flushSync } from "react-dom";
import { createRoot } from "react-dom/client";

type Order = { id: number; dish: string };

const container = document.getElementById("root");
if (container === null) throw new Error("#root topilmadi");
const root = createRoot(container);

let orders: readonly Order[] = [
  { id: 1, dish: "Osh" },
  { id: 2, dish: "Manti" },
  { id: 3, dish: "Lag'mon" },
];

function draw() {
  root.render(
    <>
      <ul>
        {orders.map((o) => (
          <li key={o.id}>
            {o.dish}:{" "}
            <input type="number" aria-label={`${o.dish} soni`} />
          </li>
        ))}
      </ul>
      <button onClick={sortOrders}>Saralash</button>
    </>
  );
}

function sortOrders() {
  orders = orders.toSorted((a, b) => a.dish.localeCompare(b.dish));
  flushSync(draw);
  for (const input of document.querySelectorAll("input")) {
    console.log(input.getAttribute("aria-label"), input.value);
  }
}

draw();

Konsolda:

text
Lag'mon soni 
Manti soni 
Osh soni 3

Ekranda:

text
Lag'mon:
Manti:
Osh:
Saralash

Ikki tuzatish. Birinchisi — key={o.id}: saralashda React qatorlarni id bo'yicha taniydi va DOM elementlarini ko'chiradi, ichidagi yozuv bilan birga. Ikkinchisi — toSorted: asl massivni buzmaydi. Hatto readonly Order[] turi sort() ni umuman taqiqlaydi: orders.sort yozilsa, tsc xato beradi.

4-mashq: Vazifalar qadami

«Vazifalar qadami» bo'limidagi hamma o'zgarishni kiriting: VazifaRoyxati.tsx, render.tsx, Vitest sozlamalari va test fayli. Keyin npm run check, npm run build va brauzerda tekshiring. Commit va PR — bo'limdagi xabar bilan.

Yechim

Tekshiruv ro'yxati:

  • npm test — node:test 146/146 va Vitest 5/5. Vitest node:test importida yiqilsa — vitest.config.ts dagi include faqat *.test.tsx ni olayotganini tekshiring.
  • npm run tip — tsconfig.json dagi references da tsconfig.test.json bormi? Bo'lmasa test fayllari tip tekshiruvidan chetda qoladi.
  • Brauzerda DevTools → Elements: <ul id="royxat"> ichidagi <li> lar React chizgani. Ularni farqlash uchun React DevTools'da komponentlar daraxtini oching (React DevTools): VazifaRoyxati → VazifaQatori → OddiyQator → Qator.
  • Vazifani belgilang: konsolda React ogohlantirishi yo'q bo'lsin. checked … without an onChange chiqsa — OddiyQator da readOnly unutilgan.
  • Bir vazifani tahrirga oching, maydonga yozing, boshqa tabda boshqa vazifani o'zgartiring — yozilgan matn joyida qolishi kerak (key + defaultValue).

Yakuniy kod — refactor: ro'yxat qatorlarini React chizadi (props, children, key) commit'i.

10. Real ishda

  • Serverdan kelgan ro'yxat — har sahifada. Backend id beradi, frontend uni key qiladi. Id yo'q API — kod ko'rigida "id qo'shing" deb qaytariladi.
  • Index key — kod ko'rigida eng ko'p belgilanadigan React xatolaridan biri. ESLint'ning eslint-plugin-react to'plamida react/no-array-index-key qoidasi bor.
  • Uzun ro'yxatlar (10 000+ qator) — barcha qatorni chizish og'ir. Ekranda ko'rinadiganlarini chizish usuli — Uzun ro'yxatlar: virtualizatsiya darsida.
  • Key bilan reset — key ni ataylab o'zgartirib, komponentni noldan yasash usuli. Bu State'ning saqlanishi va key bilan reset darsida.
  • Intervyu savollari: "key nima uchun kerak?", "Nega index key yomon?", "Key faqat ro'yxatdami?", "Math.random() ni key qilsa nima bo'ladi?".

Xulosa

  • Ro'yxat — array.map(item => <X key={item.id} … />); hisob-kitob (filter, toSorted) return dan oldin.
  • key — map qaytargan eng tashqi elementda; qardoshlar orasida noyob, barqaror, ma'lumotdan.
  • Index key o'chirish, qo'shish va saralashda yozilgan matnni boshqa qatorga "ko'chiradi".
  • Renderda crypto.randomUUID() har chizishda hamma qatorni qayta yasaydi — id'ni ma'lumot yaratilganda bering.
  • key props'ga kirmaydi; turi — string | number | bigint.
  • vazifalar da ro'yxat React'da: VazifaRoyxati + flushSync, Vitest 5/5, brauzer 159/159 — va "nechta tekshiruv o'tdi" ga ham qarash kerakligi.

Keyingi dars: Pure komponentlar va render qoidasi — komponent chizish paytida nimani qila oladi va nimani qilmasligi kerak, va StrictMode buni qanday tekshiradi.

Manbalar

  • React: "Rendering Lists", "Keeping list items in order with key" — react.dev/learn/rendering-lists
  • React: "Preserving and Resetting State" — react.dev/learn/preserving-and-resetting-state
  • Vitest: "Getting Started", environment: 'jsdom' — vitest.dev/guide
  • Testing Library: "Guiding Principles", getByRole — testing-library.com/docs
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Ro'yxatlarni render qilish va key: map, filter va nega index key xato beradi — IlmHamroh