IlmHamroh
JavaScript Full-stack/17-qism. React asoslari15/45-dars23 daqiqa
Mundarija (32)

State strukturasini loyihalash: hosilaviy qiymat, id va status

Qisqacha: Yaxshi state — kam state. Boshqa qiymatdan hisoblab olinadigan narsani (jami narx, hisob, filtrlangan ro'yxat) state'da saqlamang — renderda hisoblang. Bir ma'lumotni ikki joyda saqlamang: tanlangan taomning obyektini emas, id sini saqlang. Bir-biriga zid bo'lishi mumkin bo'lgan ikki boolean (isSending, isSent) o'rniga bitta status qiymat ishlating. Ichma-ich chuqur tuzilmani tekis qiling. Har ortiqcha state — "biri yangilandi, ikkinchisi eskicha qoldi" degan xatoga ochiq eshik.

Bu darsda

  • Qaysi qiymat state bo'lishi kerakligini, qaysi biri hisoblanishini ajrata olasiz.
  • "Props'ni state'ga ko'chirish" tuzog'ini taniysiz va initial… nomlash odatini bilasiz.
  • Tanlangan elementni obyekt bilan emas, id bilan saqlaysiz.
  • Ikki boolean o'rniga bitta status yozasiz va TypeScript bilan imkonsiz qiymatni to'sasiz.
  • Ichma-ich ma'lumotni tekis tuzilmaga aylantirasiz.
  • vazifalar dagi hisob va teglar nega state emasligini ko'rasiz.

Oldin bilishingiz kerak: State III: obyekt va massivlarni immutable yangilash, State I: useState, Holatdan UI chizish: state → render.

1. Nega bu kerak?

Oldingi darsda state'ni to'g'ri yangilashni o'rgandik: obyekt va massivni o'zgartirmasdan, yangisini yasash. Bugungi savol boshqa: state'da nima turishi kerak?

Sardor «Bahor» kassasi uchun kichik panel yozdi. Panelda ikki state bor edi: savat (qaysi taomdan nechta) va jami narx. Taom qo'shilganda ikkalasi ham yangilanardi. Bir kuni u "−" tugmasini qo'shdi va jami narxni yangilashni unutdi. Kechqurun kassada pul hisobi chiqmadi: ekranda bitta osh turibdi, jami esa 70 000 so'm.

Xato bitta qatorda edi, lekin uning ildizi chuqurroq. Jami narx alohida state bo'lgani uchun uni har o'zgarishda qo'lda yangilash kerak edi. Bir joyda unutildi — ma'lumot ikkiga bo'lindi. Agar jami har renderda savatdan hisoblanganida, bu xatoni yozishning iloji ham bo'lmasdi.

Hayotiy o'xshatish: oshxonadagi ombor daftari. Har kirgan va chiqqan qopni bitta daftarga yozasiz. Agar "jami nechta qop" ni alohida daftarga ham yozsangiz, bir kun ikkinchisiga yozish esdan chiqadi va daftarlar bir-biriga zid bo'ladi. To'g'ri yo'l — bitta daftar, jami esa kerak bo'lganda sanaladi.

state → render darsida vanilla JS'da ham shu qoidani o'rgangan edik: "hosila ma'lumotni holatda saqlamang". React'da u yanada muhim. Har state o'zgarishi qayta render chaqiradi, har setter — yana bitta "unutish mumkin bo'lgan" qator.

2. Beshta qoida

React hujjati (react.dev, "Choosing the State Structure") state tuzilmasi uchun beshta qoidani beradi. Bugun hammasini misolda ko'ramiz:

Qoida Qisqasi Nimadan saqlaydi
Bog'liqlarni guruhlang doim birga o'zgaradigan ikki qiymat — bitta obyekt birini unutish
Ziddiyatdan qoching ikki boolean o'rniga bitta status "ham yuborilmoqda, ham yuborildi"
Ortiqchasini olib tashlang hisoblab bo'ladigan narsa state emas jami eskirib qolishi
Takrorlamang obyekt emas, id ikki nusxadan biri eskirishi
Chuqur ichma-ich qilmang tekis tuzilma murakkab yangilash kodi

Hammasining ortidagi g'oya bitta: har ma'lumotning bitta egasi bo'lsin. Qolgan hamma narsa undan hisoblanadi.

2.1 Bog'liq state'ni guruhlash

Eng yengil qoidadan boshlaymiz. Stol bron qilish formasida stol raqami va mehmonlar soni doim birga tanlansa, ularni bitta obyektga qo'yish qulay:

jsx
import { useState } from "react";

function App() {
  const [table, setTable] = useState({ number: 4, guests: 2 });
  return (
    <div>
      <p>
        Stol {table.number}, {table.guests} kishi
      </p>
      <button onClick={() => setTable({ number: 7, guests: 6 })}>
        Katta stol
      </button>
    </div>
  );
}

Ekranda:

text
Stol 4, 2 kishi
Katta stol

Ikkita alohida useState ham xato emas. Lekin ular har doim birga o'zgarsa, bitta setter — bitta "unutish imkoniyati" kam. Obyektni yangilashda esa oldingi darsdagi spread qoidasini eslang: setTable({ ...table, guests: 3 }) — number yo'qolmaydi.

Teskari holat ham bor: qiymatlar alohida o'zgarsa (qidiruv matni va tanlangan filtr), ularni bitta obyektga tiqish foyda bermaydi. Har safar spread yozishga to'g'ri keladi.

3. Ortiqcha state: hisoblab bo'ladigan narsani saqlamang

3.1 Sardorning xatosi — jonli

Sardorning panelini aynan takrorlaymiz. «Ishga tushir» ni bosing va tugmalarni sinang. Pastdagi natija — avval «Osh +», keyin «Choy +», keyin «Osh −» bosilgandan keyingi holat:

jsx
import { useState } from "react";

const PRICES = { osh: 35000, choy: 5000 };

function App() {
  const [cart, setCart] = useState({ osh: 0, choy: 0 });
  const [total, setTotal] = useState(0); // ❌ ortiqcha state

  function add(dish) {
    setCart({ ...cart, [dish]: cart[dish] + 1 });
    setTotal(total + PRICES[dish]);
  }

  function remove(dish) {
    if (cart[dish] === 0) return;
    setCart({ ...cart, [dish]: cart[dish] - 1 });
    // jami narxni kamaytirish esdan chiqdi
  }

  return (
    <div>
      <p>Osh: {cart.osh}, choy: {cart.choy}</p>
      <p>Jami: {total} so'm</p>
      <button onClick={() => add("osh")}>Osh +</button>{" "}
      <button onClick={() => remove("osh")}>Osh −</button>{" "}
      <button onClick={() => add("choy")}>Choy +</button>
    </div>
  );
}

Ekranda:

text
Osh: 0, choy: 1
Jami: 40000 so'm
Osh + Osh − Choy +

Savatda bitta ko'k choy, jami esa 40 000 so'm. Qadamma-qadam:

Qadam cart total Ekranda
boshida osh 0, choy 0 0 Jami: 0
«Osh +» osh 1, choy 0 35000 Jami: 35000
«Choy +» osh 1, choy 1 40000 Jami: 40000
«Osh −» osh 0, choy 1 40000 Jami: 40000

Oxirgi qatorda ikki state bir-biriga zid. React aybdor emas: u ikkalasini ham sodiq chizdi. Xato — tuzilmada: total cart dan hisoblanadigan qiymat, lekin alohida saqlangan.

3.2 Tuzatish: renderda hisoblash

total ni state'dan olib tashlaymiz. U har renderda cart dan hisoblanadi:

jsx
import { useState } from "react";

const PRICES = { osh: 35000, choy: 5000 };

function App() {
  const [cart, setCart] = useState({ osh: 0, choy: 0 });
  // ✅ hosilaviy qiymat: har renderda savatdan hisoblanadi
  const total = cart.osh * PRICES.osh + cart.choy * PRICES.choy;

  function change(dish, step) {
    const next = cart[dish] + step;
    if (next < 0) return;
    setCart({ ...cart, [dish]: next });
  }

  return (
    <div>
      <p>Osh: {cart.osh}, choy: {cart.choy}</p>
      <p>Jami: {total} so'm</p>
      <button onClick={() => change("osh", 1)}>Osh +</button>{" "}
      <button onClick={() => change("osh", -1)}>Osh −</button>{" "}
      <button onClick={() => change("choy", 1)}>Choy +</button>
    </div>
  );
}

Ekranda:

text
Osh: 0, choy: 1
Jami: 5000 so'm
Osh + Osh − Choy +

Endi yangilash funksiyasi bitta setter chaqiradi. total ni unutishning iloji yo'q — u setter emas, oddiy const. Har setCart dan keyin React App ni qayta chaqiradi (useState darsida ko'rganimiz kabi), total ham yangidan hisoblanadi.

Hosilaviy qiymat (derived state) — boshqa state yoki props'dan hisoblab olinadigan qiymat. Uni state'da saqlash shart emas. Qoidani savol shaklida eslab qoling: "Bu qiymatni mavjud state va props'dan hisoblay olamanmi?" Javob "ha" bo'lsa — u state emas.

Hosilaviy qiymatlarga misollar:

  • jami narx, taomlar soni, "3 ta vazifa qoldi";
  • filtrlangan yoki saralangan ro'yxat (items.filter(…));
  • "forma to'g'ri to'ldirilganmi" (name.trim() !== "");
  • ism va familiyadan to'liq ism.

3.3 "Har renderda hisoblash sekin emasmi?"

Ko'pincha yo'q. Bir necha mingta elementdan filter yoki yig'indi — millisekund ulushlari. Biz vazifalar da 1 000 vazifa bilan o'lchaganmiz: qidiruvda har harfda butun ilova qayta chizilishi va hisob, teglar, ko'rinadigan qatorlar yangidan hisoblanishi bilan birga — taxminan 16–19 ms (Chrome 154, oddiy noutbuk). Bu ko'z ilg'amaydigan vaqt. To'liq o'lchov — mini-ilova darsida.

Hisoblash haqiqatan og'ir bo'lsa (o'lchab ko'rilgan bo'lsa!), natijani eslab qolish usuli bor — useMemo. Uni memo, useMemo, useCallback darsida o'rganamiz. Muhimi: useMemo ham state emas, u ham hisoblaydi — faqat keshlaydi.

Tekshirib ko'ring: Komponentda const [items, setItems] (taomlar ro'yxati) va const [count, setCount] (taomlar soni) bor. Qaysi biri ortiqcha va u nima bilan almashtiriladi?

Javob

count ortiqcha: u items.length ga teng. setCount ni olib tashlab, renderda const count = items.length; yozamiz. Aks holda taom o'chirilganda setCount ni unutish — Sardorning xatosi.

3.4 Props'ni state'ga ko'chirish tuzog'i

Ortiqcha state'ning yashirin turi bor: props'ni state'ga nusxalash. Narx yorlig'i komponenti narxni props'dan olib, state'ga yozadi:

jsx
import { useState } from "react";

function PriceTag({ price }) {
  const [shown] = useState(price); // ❌ props nusxasi
  return <p>Yorliqda: {shown} so'm</p>;
}

function App() {
  const [price, setPrice] = useState(35000);
  return (
    <div>
      <PriceTag price={price} />
      <p>Menyuda: {price} so'm</p>
      <button onClick={() => setPrice(price + 1000)}>
        Narxni oshirish
      </button>
    </div>
  );
}

Ekranda:

text
Yorliqda: 35000 so'm
Menyuda: 36000 so'm
Narxni oshirish

Menyuda narx o'zgardi, yorliqda — yo'q. Sabab: useState(price) dagi qiymat faqat birinchi renderda olinadi. Keyingi renderlarda React argumentga qaramaydi — state o'zining xotirasida. Ota yangi price yuborsa ham, shown o'zgarmaydi.

Tuzatish — state'ni olib tashlash. price ni to'g'ridan-to'g'ri ishlating:

jsx
function PriceTag({ price }) {
  return <p>Yorliqda: {price} so'm</p>; // ✅ har doim yangi
}

Ba'zan nusxa ataylab kerak bo'ladi: forma boshida props'dagi qiymat bilan to'ladi, keyin foydalanuvchi uni o'zi o'zgartiradi. Bunday props'ni initial… yoki default… deb nomlash odat bo'lgan: initialPrice, defaultName. Nom "keyingi o'zgarishlarga qaramayman" deb ochiq aytadi. vazifalar dagi tahrir maydoni Formalar I darsida aynan shunday bo'ladi: useState(vazifa.matn) — tahrir boshlanganda matn olinadi, keyin foydalanuvchi o'zi yozadi. Props o'zgarganda state'ni noldan boshlash kerak bo'lsa — key usuli bor, u State'ning saqlanishi darsida.

4. Takrorlash: obyekt emas, id saqlang

4.1 Tanlangan taom eskirib qoldi

Menyuda taomni tanlash mumkin, tanlangani pastda ko'rinadi. Har qatorda narxni 1 000 so'mga oshiradigan tugma ham bor. Sardor tanlangan taomni butun obyekti bilan saqladi:

jsx
import { useState } from "react";

const START = [
  { id: 1, name: "Osh", price: 35000 },
  { id: 2, name: "Manti", price: 30000 },
];

function App() {
  const [items, setItems] = useState(START);
  const [selected, setSelected] = useState(null); // ❌ nusxa

  function raise(id) {
    setItems(items.map((it) =>
      it.id === id ? { ...it, price: it.price + 1000 } : it
    ));
  }

  return (
    <div>
      <ul>
        {items.map((it) => (
          <li key={it.id}>
            {it.name} — {it.price}{" "}
            <button onClick={() => setSelected(it)}>
              {it.name} tanlash
            </button>{" "}
            <button onClick={() => raise(it.id)}>
              {it.name} +1000
            </button>
          </li>
        ))}
      </ul>
      <p>Tanlangan: {selected ? selected.price : "yo'q"}</p>
    </div>
  );
}

Ekranda:

text
Osh — 36000 Osh tanlash Osh +1000
Manti — 30000 Manti tanlash Manti +1000
Tanlangan: 35000

Ro'yxatda osh 36 000, pastda — 35 000. Endi bir xil oshning ikki nusxasi bor: biri items ichida, ikkinchisi selected da. raise yangi obyekt yasadi (immutable yangilash to'g'ri ishladi), lekin selected hali eski obyektga qarab turibdi.

Qadam items[0].price selected Ekranda pastda
boshida 35000 null Tanlangan: yo'q
«Osh tanlash» 35000 osh obyekti (35000) Tanlangan: 35000
«Osh +1000» 36000 (yangi obyekt) eski obyekt (35000) Tanlangan: 35000

4.2 Tuzatish: id va hisoblangan obyekt

Tanlangan taomning o'zini emas, faqat id sini saqlaymiz. Obyektning o'zi esa har renderda ro'yxatdan topiladi:

jsx
import { useState } from "react";

const START = [
  { id: 1, name: "Osh", price: 35000 },
  { id: 2, name: "Manti", price: 30000 },
];

function App() {
  const [items, setItems] = useState(START);
  const [selectedId, setSelectedId] = useState(null); // ✅ faqat id
  const selected = items.find((it) => it.id === selectedId);

  function raise(id) {
    setItems(items.map((it) =>
      it.id === id ? { ...it, price: it.price + 1000 } : it
    ));
  }

  return (
    <div>
      <ul>
        {items.map((it) => (
          <li key={it.id}>
            {it.name} — {it.price}{" "}
            <button onClick={() => setSelectedId(it.id)}>
              {it.name} tanlash
            </button>{" "}
            <button onClick={() => raise(it.id)}>
              {it.name} +1000
            </button>
          </li>
        ))}
      </ul>
      <p>Tanlangan: {selected ? selected.price : "yo'q"}</p>
    </div>
  );
}

Ekranda:

text
Osh — 36000 Osh tanlash Osh +1000
Manti — 30000 Manti tanlash Manti +1000
Tanlangan: 36000

selected endi hosilaviy qiymat: selectedId va items dan hisoblanadi. Narx qayerda o'zgarsa ham, pastdagi qator ergashadi.

Id saqlashning yana bir foydasi bor. Tanlangan taom ro'yxatdan o'chirilsa, items.find undefined qaytaradi. Ekranda "yo'q" chiqadi — o'chgan taom "arvoh" bo'lib qolmaydi. Obyekt saqlaganda esa o'chirilgan osh pastda tanlangan holda turaverardi.

Maslahat: Bir nechta tanlov (masalan, belgilangan taomlar) uchun ham shu qoida: obyektlar massivi emas, id'lar to'plami — new Set([1, 3]) yoki [1, 3]. Tekshiruv: selectedIds.has(it.id).

5. Qarama-qarshi flaglar o'rniga status

5.1 Ham yuborilmoqda, ham yuborildi

Buyurtma tugmasi bosilganda "Yuborilmoqda…" yozuvi chiqadi, yarim soniyadan keyin "Qabul qilindi!" ko'rinadi. Sardor ikkita boolean yozdi:

jsx
import { useState } from "react";

function App() {
  const [isSending, setIsSending] = useState(false);
  const [isSent, setIsSent] = useState(false);

  function send() {
    setIsSending(true);
    setTimeout(() => {
      setIsSent(true);
      // setIsSending(false) esdan chiqdi
    }, 500);
  }

  return (
    <div>
      <button onClick={send}>Buyurtma berish</button>
      {isSending && <p>Yuborilmoqda…</p>}
      {isSent && <p>Qabul qilindi!</p>}
    </div>
  );
}

Ekranda:

text
Buyurtma berish
Yuborilmoqda…
Qabul qilindi!

Ikki yozuv birga chiqdi. Ikki boolean — to'rtta mumkin bo'lgan holat: false/false, true/false, false/true va true/true. Oxirgisi — "ham yuborilmoqda, ham yuborildi" — ma'nosiz. Lekin kod uni yaratishga yo'l qo'yadi va bir qatorni unutish yetarli. Shu sabab yuborilayotganda tugma ham bloklanmagan — ikkinchi bosish ikkinchi buyurtmani yuborardi.

5.2 Bitta status — faqat ma'noli holatlar

Haqiqatda buyurtmaning uchta holati bor: tahrirda, yuborilmoqda, yuborildi. Ularni bitta qiymat bilan ifodalaymiz:

jsx
import { useState } from "react";

function App() {
  // "tahrir" | "yuborilmoqda" | "yuborildi"
  const [status, setStatus] = useState("tahrir");

  function send() {
    setStatus("yuborilmoqda");
    setTimeout(() => setStatus("yuborildi"), 500);
  }

  if (status === "yuborildi") {
    return <p>Qabul qilindi!</p>;
  }
  return (
    <div>
      <button onClick={send} disabled={status === "yuborilmoqda"}>
        Buyurtma berish
      </button>
      {status === "yuborilmoqda" && <p>Yuborilmoqda…</p>}
    </div>
  );
}

Ekranda:

text
Qabul qilindi!
Qadam status Ekranda
boshida "tahrir" Buyurtma berish
bosildi "yuborilmoqda" tugma (bloklangan), Yuborilmoqda…
500 ms dan keyin "yuborildi" Qabul qilindi!

Endi "ham yuborilmoqda, ham yuborildi" holatini yozib bo'lmaydi: status bir vaqtda faqat bitta qiymat. Tugmaning bloklanishi ham status'dan hisoblanadi (disabled={status === "yuborilmoqda"}) — alohida isDisabled state kerak emas.

Bunday qiymat ko'pincha status enum deyiladi: "enum" — oldindan sanab chiqilgan qiymatlar to'plami (enum va uning muqobillari darsida TypeScript'dagi shaklini ko'rgansiz). Boolean'lar hali ham kerak bo'lsa, ularni status'dan hisoblang: const isSending = status === "yuborilmoqda";.

5.3 TypeScript imkonsiz qiymatni to'xtatadi

JavaScript'da setStatus("yubordi") (harf xatosi) jim o'tadi va ekran hech qaysi shartga tushmaydi. TypeScript'da status turini literal union bilan yozamiz:

tsx
import { useState } from "react";

type Status = "tahrir" | "yuborilmoqda" | "yuborildi" | "xato";

export function App() {
  const [status, setStatus] = useState<Status>("tahrir");
  return (
    <button onClick={() => setStatus("yubordi")}>{status}</button>
  );
}

Xato:

text
TS2345: Argument of type '"yubordi"' is not assignable to parameter of type 'SetStateAction<Status>'.

""yubordi" turidagi argumentni SetStateAction<Status> turidagi parametrga berib bo'lmaydi." SetStateAction<Status> — setter qabul qiladigan narsa: yo Status qiymati, yo prev => Status funksiyasi (snapshot va updater darsida ko'rgan shakl). useState<Status> dagi <Status> — generic (Generics): "bu state faqat shu to'rtta satrdan biri".

Har statusga matn kerak bo'lsa, Record<Status, string> yozing — bittasini unutsangiz, tsc aytadi. vazifalar da aynan shunday: Record<Filtr, string> bilan bo'sh ro'yxat matnlari.

Tekshirib ko'ring: Rasm yuklash komponentida uchta boolean bor: isLoading, isError, isDone. Nechta kombinatsiya mumkin va nechta haqiqiy holat bor?

Javob

Uch boolean — 2 × 2 × 2 = 8 ta kombinatsiya. Haqiqiy holatlar esa to'rtta: kutish (hech narsa boshlanmagan), yuklanmoqda, xato, tayyor. Qolgan to'rttasi (masalan "yuklanmoqda va xato") — imkonsiz holatlar. Bitta status: "kutish" | "yuklanmoqda" | "xato" | "tayyor" ularni yo'qotadi.

6. Chuqur ichma-ich tuzilmani tekislash

6.1 Ichma-ich menyu

«Bahor» menyusi bo'limlarga bo'lingan: issiq taomlar, ichimliklar. Birinchi xayolga keladigan tuzilma — ichma-ich:

js
const menu = [
  {
    id: "issiq",
    title: "Issiq taomlar",
    items: [
      { id: 1, name: "Osh" },
      { id: 2, name: "Manti" },
    ],
  },
  {
    id: "ichimlik",
    title: "Ichimliklar",
    items: [{ id: 3, name: "Ko'k choy" }],
  },
];

// Mantini o'chirish: avval bo'limni, keyin ichidagi taomni topish
function removeItem(menu, itemId) {
  return menu.map((section) => ({
    ...section,
    items: section.items.filter((it) => it.id !== itemId),
  }));
}

const next = removeItem(menu, 2);
console.log(next[0].items.map((it) => it.name)); // [ 'Osh' ]
console.log(menu[0].items.length); // 2

Ishlaydi, lekin har amal ikki qavat chuqurlikda: map ichida filter, ikki spread. Taomni bir bo'limdan boshqasiga ko'chirish yoki narxni o'zgartirish yanada uzun bo'ladi. Uch qavatli tuzilmada (bo'lim → taom → qo'shimcha) bu kod tez chalkashib ketadi. Yana bir xavf: bitta taom ikki bo'limda (masalan, "Tavsiya" va "Issiq taomlar") bo'lsa, u ikki nusxada saqlanadi — bu «Takrorlash» bo'limidagi muammo.

6.2 Tekis tuzilma

Tekis (normalized) tuzilma — har ma'lumot bitta ro'yxatda, bog'lanish esa id orqali. Taom o'z bo'limini sectionId bilan "eslaydi":

jsx
import { useState } from "react";

const SECTIONS = [
  { id: "issiq", title: "Issiq taomlar" },
  { id: "ichimlik", title: "Ichimliklar" },
];

const START = [
  { id: 1, name: "Osh", sectionId: "issiq" },
  { id: 2, name: "Manti", sectionId: "issiq" },
  { id: 3, name: "Ko'k choy", sectionId: "ichimlik" },
];

function App() {
  const [items, setItems] = useState(START);

  function remove(id) {
    setItems(items.filter((it) => it.id !== id)); // bitta qavat
  }

  return SECTIONS.map((s) => (
    <section key={s.id}>
      <h3>{s.title}</h3>
      {items
        .filter((it) => it.sectionId === s.id)
        .map((it) => (
          <p key={it.id}>
            {it.name}{" "}
            <button onClick={() => remove(it.id)}>
              {it.name} o'chirish
            </button>
          </p>
        ))}
    </section>
  ));
}

Ekranda:

text
Issiq taomlar
Osh Osh o'chirish
Ichimliklar
Ko'k choy Ko'k choy o'chirish

O'chirish — bitta filter. Bo'limlar ro'yxati umuman o'zgarmaydi (u state ham emas — konstanta). Taomni ko'chirish — bitta maydonni almashtirish: { ...it, sectionId: "ichimlik" }. Ekrandagi guruhlash esa renderda hisoblanadi.

Katta ilovalarda tekis tuzilma ko'pincha { byId: { 1: {…}, 2: {…} }, ids: [1, 2] } ko'rinishida bo'ladi — id bo'yicha qidirish massivdagi find dan tez. State kutubxonalari bilan ishlaganda (Redux Toolkit II) shu shaklni ko'p uchratasiz. Kichik ilova uchun sectionId li massiv yetarli.

flowchart LR
  subgraph I["Ichma-ich"]
    B1["Bo'lim"] --> T1["Taom"]
    B2["Bo'lim"] --> T2["Taom (nusxa)"]
  end
  subgraph T["Tekis"]
    TT["Taomlar ro'yxati"] -- "sectionId" --> BB["Bo'limlar ro'yxati"]
  end

Chapda bitta taom ikki joyda yashashi mumkin. O'ngda har taom bir marta saqlanadi, bog'lanish esa id bilan.

7. Vazifalar qadami: hisob va teglar — state emas

7.1 Oldin: DOM — xotira

vazifalar hozir (State III darsidagi holat) shunday ishlaydi: ro'yxat qatorlarini va yangi vazifa formasini React chizadi, qolgan qismni esa render.tsx dagi vanilla funksiyalar yozadi. Ular orasida — hisobniYoz, teglarniYoz, boshXabarniYoz, qidiruvniYoz. Har biri hisoblaydi va natijani DOM'ga yozadi. Bitta qiziq qator bor:

ts
function boshXabarniYoz(): void {
  boshXabar.hidden = royxat.children.length > 0;
  // …
}

"Ro'yxat bo'shmi?" savoliga DOM javob beradi: <ul> da nechta bola bor. Ya'ni ma'lumotning ikkinchi nusxasi — sahifaning o'zida. Shuning uchun bu funksiyani ro'yxat chizilgandan keyin chaqirish shart edi (render() ichida tartib muhim). Yana bir yordamchi — matnniYoz — bir xil matnni qayta yozmaslik uchun DOM'dagi matnni yangisi bilan solishtirardi. Ekran o'quvchi o'zgarmagan hisobni qayta o'qimasin, deb shunday qilingan.

7.2 Keyin: renderda hisoblanadi

Bu qism ikki darsda bitta o'zgarish bo'lib bajariladi. Bugun — reja va birinchi komponent; keyingi darsda — butun UI bitta App ostida va commit. Yangi branch oching: refactor/app-ildizi.

Hisob va teglar qatori uchun src/Statistika.tsx komponenti (kanondan aynan):

tsx
// Statistika.tsx — hisob va teglar qatori. Hammasi ro'yxatdan
// hisoblanadi (hosilaviy qiymat, 17/#15) — alohida saqlanmaydi
import { teglarMatni } from "./teglar.ts";

type Props = {
  qoldi: number;
  bajarildi: number;
  teglar: ReadonlyMap<string, number>; // teg → nechta vazifada
  // faqat bajarilganlarda qolgan teglar:
  yopilgan: ReadonlySet<string>;
};

export function Statistika({
  qoldi,
  bajarildi,
  teglar,
  yopilgan,
}: Props) {
  const qoshimcha =
    yopilgan.size > 0 ? `. Yopilgan: ${[...yopilgan].join(", ")}` : "";
  return (
    <>
      <p id="hisob" aria-live="polite">
        {`${qoldi} ta vazifa qoldi, ${bajarildi} ta bajarildi`}
      </p>
      <p id="teglar" hidden={teglar.size === 0}>
        {`Teglar: ${teglarMatni(teglar)}${qoshimcha}`}
      </p>
    </>
  );
}

Statistika hech narsani saqlamaydi — props'dan matn yasaydi. qoshimcha — yana bir hosilaviy qiymat. teglarMatni — teglar.ts dagi sof funksiya (render.tsx dan ko'chadi). aria-live="polite" — ARIA darsidagi jonli hudud: hisob o'zgarsa, Malikaning ekran o'quvchisi uni o'qib beradi.

Hisoblashning o'zi App da bo'ladi. Kelajakdagi App.tsx dan parcha (to'liq fayl — keyingi darsda):

tsx
  // Hosilaviy qiymatlar (17/#15): har renderda vazifalardan
  // hisoblanadi, state'da saqlanmaydi — "ro'yxat o'zgardi, hisob
  // esa eski" degan xato bo'lishi mumkin emas
  const royxat = VazifalarRoyxati.dan(vazifalar);
  const korinadigan = korinadiganVazifalar(
    vazifalar,
    filtr,
    saralash,
    qidiruv,
  );
  const { qoldi, bajarildi } = royxat.statistika();
  const teglar = royxat.tegHisobi();
  const daraxt = tegDaraxtiniQur(teglar);

Beshta hosilaviy qiymat, birortasi ham state emas. Endi "bo'sh xabar" ham DOM'ga emas, korinadigan.length ga qaraydi:

tsx
      <p id="bosh-xabar" hidden={korinadigan.length > 0}>

matnniYoz ham kerak bo'lmay qoladi. React matn tugunini faqat matn o'zgarganda yozadi — solishtirishni o'zi qiladi. Ekran o'quvchi o'zgarmagan hisobni qayta eshitmaydi.

7.3 Kanondagi boshqa qarorlar — shu dars ko'zi bilan

holat.ts dagi Holat turini ochib, bugungi qoidalarni qidiring:

ts
export type Holat = {
  vazifalar: readonly VazifaMalumoti[];
  filtr: Filtr;
  qidiruv: string;
  saralash: Saralash;
  tahrirId: number | null;
  tarmoq: TarmoqHolati;
  // …
};
  • tahrirId: number | null — tahrirdagi vazifaning id si, obyekti emas. Fon yangilanishi (server, boshqa tab) vazifa matnini almashtirsa ham, "qaysi qator tahrirda" degan javob buzilmaydi.
  • tarmoq: TarmoqHolati — "yuklanmoqda" | "tayyor" | "xato" | "oflayn". To'rtta boolean emas, bitta status.
  • Hisob, teglar, ko'rinadigan vazifalar — Holat da yo'q. Ular hosilaviy.

Bu tur TypeScript qismida shunday yozilgan edi — React'ga o'tishda u o'zgarmadi. Yaxshi state tuzilmasi kutubxonaga bog'liq emas.

8. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
jami yoki hisob eskirib qoladi hosilaviy qiymat alohida state'da renderda hisoblang
bola komponent ota o'zgarishini ko'rmaydi useState(props.x) — nusxa props'ni to'g'ridan-to'g'ri ishlating yoki initialX deb nomlang
tanlangan element eski ma'lumotni ko'rsatadi obyekt nusxasi state'da id saqlang, obyektni find bilan oling
ekranda ikki zid yozuv ikki boolean flag bitta status
setStatus("yubordi") jim o'tadi JS'da tur yo'q TS literal union: useState<Status>
yangilash kodi uch qavat spread chuqur ichma-ich tuzilma tekis ro'yxat + id bog'lanish

9. Mashqlar

Mashqlar kurs/mashqlar/17/15-state-tuzilmasi/ da. Har yechimni «Ishga tushir» bilan sinang.

1-mashq (oson): Ortiqchasini toping

Kafe xodimi uchun smena paneli. Unda beshta state bor:

jsx
const [orders, setOrders] = useState([]);       // buyurtmalar
const [orderCount, setOrderCount] = useState(0);
const [firstName, setFirstName] = useState("Sardor");
const [lastName, setLastName] = useState("Aliyev");
const [fullName, setFullName] = useState("Sardor Aliyev");
  • Qaysi state orders dan hisoblanadi:
  • Qaysi state ism va familiyadan hisoblanadi:
  • Tuzatilgandan keyin nechta useState qoladi:
Yechim

orderCount — orders.length, fullName — `${firstName} ${lastName}`. Ikkalasi hosilaviy, renderda const bilan hisoblanadi. Uchta useState qoladi: orders, firstName, lastName.

2-mashq (o'rta): Navbatdagi stol

Ofitsiant ekranida stollar ro'yxati va "Hozir xizmatda" qatori bor. Har stolga "Xizmat" tugmasi (shu stolni tanlaydi) va "+1 mehmon" tugmasi bor. Sardor tanlangan stolni obyekt bilan saqladi va "+1 mehmon" dan keyin pastdagi qator eskirib qoladi. Kodni id bilan tuzating. Ishora: «Obyekt emas, id saqlang» bo'limidagi find.

jsx
const [tables, setTables] = useState([
  { id: 4, guests: 2 },
  { id: 7, guests: 5 },
]);
const [current, setCurrent] = useState(null);
// …
<p>
  Hozir xizmatda:{" "}
  {current ? `${current.id}-stol` : "hech kim"}
</p>
Yechim
jsx
import { useState } from "react";

function App() {
  const [tables, setTables] = useState([
    { id: 4, guests: 2 },
    { id: 7, guests: 5 },
  ]);
  const [currentId, setCurrentId] = useState(null);
  const current = tables.find((t) => t.id === currentId);

  function addGuest(id) {
    setTables(tables.map((t) =>
      t.id === id ? { ...t, guests: t.guests + 1 } : t
    ));
  }

  return (
    <div>
      {tables.map((t) => (
        <p key={t.id}>
          {t.id}-stol: {t.guests} kishi{" "}
          <button onClick={() => setCurrentId(t.id)}>
            {t.id}-stol xizmat
          </button>{" "}
          <button onClick={() => addGuest(t.id)}>
            {t.id}-stol +1
          </button>
        </p>
      ))}
      <p>
        Hozir xizmatda:{" "}
        {current
          ? `${current.id}-stol, ${current.guests} kishi`
          : "hech kim"}
      </p>
    </div>
  );
}

Ekranda:

text
4-stol: 3 kishi 4-stol xizmat 4-stol +1
7-stol: 5 kishi 7-stol xizmat 7-stol +1
Hozir xizmatda: 4-stol, 3 kishi

currentId — state, current — hosilaviy. Pastdagi qator har renderda ro'yxatdan olinadi, shuning uchun "3 kishi" darhol ko'rinadi.

3-mashq (qiyin): Bron formasining statusi

Stol bron qilish komponentida uchta boolean bor: isChecking (bo'sh joy tekshirilmoqda), isBooked (bron qilindi), isFull (joy yo'q). TypeScript'da bitta status bilan qayta yozing:

  • type BookingStatus — to'rtta qiymat: "tanlash", "tekshirilmoqda", "bron", "joy-yoq";
  • Record<BookingStatus, string> bilan har holat matni;
  • «Bron qilish» tugmasi "tekshirilmoqda" da bloklangan bo'lsin (disabled — hosilaviy);
  • tugma bosilganda 300 ms dan keyin: mehmonlar soni 8 dan ko'p bo'lsa — "joy-yoq", aks holda "bron". Mehmonlar soni — const guests = 4; konstanta.

Ishora: Record<K, V> — Index signature va Record darsida; setTimeout — status ikki marta o'zgaradi.

Yechim
tsx
import { useState } from "react";

type BookingStatus =
  | "tanlash"
  | "tekshirilmoqda"
  | "bron"
  | "joy-yoq";

const MESSAGES: Record<BookingStatus, string> = {
  tanlash: "Stol tanlang",
  tekshirilmoqda: "Bo'sh joy tekshirilmoqda…",
  bron: "Stol bron qilindi!",
  "joy-yoq": "Afsuski, joy yo'q",
};

const guests = 4;

export default function App() {
  const [status, setStatus] =
    useState<BookingStatus>("tanlash");
  const isChecking = status === "tekshirilmoqda"; // hosilaviy

  function book() {
    setStatus("tekshirilmoqda");
    setTimeout(() => {
      setStatus(guests > 8 ? "joy-yoq" : "bron");
    }, 300);
  }

  return (
    <div>
      <p>{guests} kishi uchun</p>
      <button onClick={book} disabled={isChecking}>
        Bron qilish
      </button>
      <p>{MESSAGES[status]}</p>
    </div>
  );
}

Ekranda:

text
4 kishi uchun
Bron qilish
Stol bron qilindi!

Uch boolean'dagi sakkizta kombinatsiya o'rniga to'rtta ma'noli holat. MESSAGES dan bitta kalitni o'chirsangiz, tsc TS2741 (Property '"joy-yoq"' is missing…) bilan to'xtatadi.

4-mashq: Vazifalar qadami

vazifalar da refactor/app-ildizi branch'ini oching. «Vazifalar qadami» bo'limidagidek src/Statistika.tsx ni yarating va render.tsx dagi hisobniYoz va teglarniYoz hisoblayotgan qiymatlarni yozib chiqing: qaysilari holat dan, qaysilari hosilaviy. Commit hozircha qilinmaydi — komponent keyingi darsda App ga ulanadi.

Yechim

Ro'yxat: qoldi, bajarildi (royxat.statistika()), teglar (royxat.tegHisobi()), yopilgan (royxat.yopilganTeglar()), qoshimcha matn — hammasi holat.vazifalar dan hisoblanadi. holat dan to'g'ridan-to'g'ri faqat vazifalar olinadi. npm run tip — Statistika.tsx hali hech qayerda import qilinmagan bo'lsa ham tip xatosiz bo'lishi kerak. knip esa "ishlatilmagan fayl" deydi — bu keyingi darsda yo'qoladi.

10. Real ishda

  • Kod ko'rigida (code review) eng ko'p yoziladigan izohlardan biri: "Bu state kerak emas, hisoblasa bo'ladi". Ayniqsa effekt bilan state'ni sinxronlash (useEffect(() => setTotal(…))) — bu ham ortiqcha state, buni Sizga effekt kerak emas darsida ko'ramiz.
  • Status maydonlari — server bilan ishlaydigan har komponentda: "idle" | "loading" | "success" | "error". TanStack Query kabi kutubxonalar ham aynan shunday status qaytaradi (19-qism).
  • Normalizatsiya — Redux Toolkit'ning createEntityAdapter i va ma'lumotlar bazasi (SQL'dagi jadval va tashqi kalit) shu g'oyaga qurilgan. Holat mashinasi fikrlashi — Reducer + Immer darsida.
  • Intervyu savollari: "Derived state nima va nega uni saqlamaslik kerak?", "Props'ni state'ga ko'chirish nega xavfli?", "isLoading va isError ni bitta qiymat bilan qanday ifodalaysiz?".

Xulosa

  • State — faqat o'zgaradigan va hisoblab bo'lmaydigan ma'lumot; qolgani renderda hisoblanadi.
  • useState(props.x) faqat birinchi qiymatni oladi; nusxa ataylab bo'lsa — initialX deb nomlang.
  • Tanlangan element — id; obyektning o'zi find bilan har renderda topiladi.
  • Bir-biriga zid bo'lishi mumkin bo'lgan boolean'lar o'rniga bitta status; TS literal union imkonsiz qiymatni to'sadi.
  • Chuqur ichma-ich tuzilma o'rniga tekis ro'yxat va id bog'lanish.
  • vazifalar da hisob, teglar va ko'rinadigan vazifalar — App renderida hisoblanadi, DOM'dan o'qilmaydi.

Keyingi dars: State'ni ko'tarish va yagona haqiqat manbai — ikki komponent bir ma'lumotni ulashsa, state qayerda turishi kerak; vazifalar ning butun UI'si bitta App ostiga yig'iladi.

Manbalar

  • React: "Choosing the State Structure" — react.dev/learn/choosing-the-state-structure
  • React: "Thinking in React" (3-qadam: minimal state) — react.dev/learn/thinking-in-react
  • React: useState — react.dev/reference/react/useState
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
State strukturasini loyihalash: hosilaviy qiymat, id va status — IlmHamroh