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

State III: obyekt va massivlarni immutable yangilash — spread, map, filter va Immer

Qisqacha: React state o'zgarganini havola bo'yicha (Object.is) aniqlaydi. list.push(x); setList(list) — o'sha massiv, React "o'zgarish yo'q" deb qayta chizmaydi. Shuning uchun state'dagi obyekt va massivni joyida o'zgartirmaymiz — yangisini yasaymiz: obyektga { ...order, count: 3 }, massivga qo'shish [...list, x], o'chirish filter, o'zgartirish map, saralash toSorted. Ichma-ich obyektda har qavat nusxalanadi. Tegilmagan elementlar esa o'sha-o'sha qoladi — nusxa arzon. Chuqur tuzilmalar uchun Immer "mutatsiya qilgandek" yozib, o'zgarmas natija beradi.

Bu darsda

  • Nega push yoki obj.x = … dan keyin ekran yangilanmasligini Object.is bilan tushuntira olasiz.
  • Obyekt state'ni spread bilan, ichma-ich obyektni har qavatini nusxalab yangilaysiz.
  • Massivda qo'shish, o'chirish, o'zgartirish, saralash va almashtirishni o'zgartirmaydigan metodlar bilan qilasiz.
  • Sayoz nusxaning tuzog'ini taniysiz, structuredClone va Immer qachon kerakligini bilasiz.
  • vazifalar ga sof amallar.ts va readonly ro'yxatni qo'shib, branch'ni commit qilasiz.

Oldin bilishingiz kerak: State II: snapshot, batching va updater funksiya, Ichma-ich ma'lumotni immutable yangilash, Nusxa bilan o'zgartirish: toSorted, toReversed, toSpliced, with, Tenglik: ==, === va Object.is.

1. Nega bu kerak?

Sardor menyu muharririni yozdi: "Manti qo'shish" tugmasi. JavaScript odati bilan:

jsx
import { useState } from "react";

function App() {
  const [dishes, setDishes] = useState(["Osh"]);
  console.log("chizildi:", dishes.length);

  function add() {
    dishes.push("Manti"); // ❌ o'sha massivni o'zgartiryapti
    setDishes(dishes);
    console.log("massivda:", dishes.length);
  }

  return (
    <>
      <p>{dishes.join(", ")}</p>
      <button onClick={add}>Manti qo'shish</button>
    </>
  );
}

Konsolda:

text
chizildi: 1
massivda: 2

Ekranda:

text
Osh
Manti qo'shish

Massivda ikki taom, ekranda bitta. "chizildi" ham ikkinchi marta chiqmadi. Xato xabari yo'q — shuning uchun bu React'dagi eng ko'p uchraydigan va eng uzoq qidiriladigan xatolardan biri.

Sababi — Havola semantikasi darsidan. setDishes(dishes) React'ga o'sha massivni berdi. React esa eski va yangi qiymatni Object.is bilan solishtiradi: bir xil havola — "hech narsa o'zgarmagan", chizish shart emas. Ichida nima o'zgargani React'ni qiziqtirmaydi — u ichiga qaramaydi.

Hayotiy o'xshatish: kassadagi chek. Buyurtma o'zgarsa, kassir eski chekka ruchka bilan yozib qo'ymaydi — yangi chek chiqaradi. Oshxonaga yangi chek kelsa, darhol ko'radi: "bu boshqa chek — demak o'zgarish bor". Eski chek esa arxivda o'zgarmay qoladi. React ham shunday: yangi havola — yangi chek.

2. Nega aynan havola

React har elementni chuqur solishtirmaydi — bu juda qimmat bo'lardi: 1 000 vazifali ro'yxatda har bosishda 1 000 obyektni maydonma-maydon tekshirish. Havolani solishtirish esa bitta amal. Lekin bu ishlashi uchun kelishuv kerak: state'dagi obyekt va massiv o'zgarmas (immutable). O'zgarish bo'lsa — yangi obyekt, bo'lmasa — o'sha.

Kod Havola React
list.push(x); setList(list) o'sha qayta chizmaydi
setList([...list, x]) yangi qayta chizadi
order.count = 3; setOrder(order) o'sha qayta chizmaydi
setOrder({ ...order, count: 3 }) yangi qayta chizadi

Kelishuvning foydasi faqat React uchun emas. O'zgarmas ma'lumotda "oldingi holat" har doim butun qoladi: bekor qilish (undo), solishtirish, xatoni qayta takrorlash oson. Buni Immutability chuqur darsida ko'rgandik. vazifalar da ham bugun shu foyda paydo bo'ladi — «Vazifalar qadami» bo'limida.

3. Obyekt state

3.1 Spread bilan yangi obyekt

jsx
import { useState } from "react";

function App() {
  const [order, setOrder] = useState({
    dish: "Osh",
    count: 1,
    withTea: false,
  });

  return (
    <>
      <p>
        {order.dish} × {order.count}
        {order.withTea && " + ko'k choy"}
      </p>
      <button
        onClick={() => setOrder({ ...order, count: order.count + 1 })}
      >
        Yana bitta
      </button>
      <button onClick={() => setOrder({ ...order, withTea: true })}>
        Choy bilan
      </button>
    </>
  );
}

Ekranda:

text
Osh × 3 + ko'k choy
Yana bittaChoy bilan

{ ...order, count: order.count + 1 } — spread: eski maydonlarning hammasi, keyin count ustidan yoziladi. Tartib muhim: spread oldin, o'zgargan maydon keyin (Props dagi qoidaning o'zi).

Qadam State Ekranda
boshida {dish: "Osh", count: 1, withTea: false} Osh × 1
«Yana bitta» yangi obyekt, count: 2 Osh × 2
«Yana bitta» yangi obyekt, count: 3 Osh × 3
«Choy bilan» yangi obyekt, withTea: true Osh × 3 + ko'k choy

Har qadamda — yangi obyekt. Eskisini hech kim o'zgartirmadi.

Maslahat: Bir nechta mustaqil qiymat bitta obyektda bo'lishi shart emas. count va withTea ni ikkita useState ga ajratsa ham bo'ladi — spread kerak bo'lmaydi. Qachon obyekt, qachon alohida state — State strukturasini loyihalash darsida.

3.2 Ichma-ich obyekt: har qavat nusxalanadi

Buyurtmada ofitsiant ma'lumoti ham bor:

jsx
import { useState } from "react";

function App() {
  const [order, setOrder] = useState({
    table: 4,
    waiter: { name: "Sardor", shift: "kunduzgi" },
  });

  function changeShift() {
    setOrder({
      ...order, // 1-qavat: buyurtma
      waiter: {
        ...order.waiter, // 2-qavat: ofitsiant
        shift: "kechki",
      },
    });
  }

  return (
    <>
      <p>
        {order.table}-stol, {order.waiter.name} ({order.waiter.shift})
      </p>
      <button onClick={changeShift}>Smenani almashtir</button>
    </>
  );
}

Ekranda:

text
4-stol, Sardor (kechki)
Smenani almashtir

O'zgargan maydongacha bo'lgan har qavat yangi obyekt bo'ladi: buyurtma va ofitsiant. { ...order, waiter: { ...order.waiter, shift } } — Ichma-ich ma'lumotni immutable yangilash darsidagi naqsh. Qavatlardan birini unutsangiz — order.waiter.shift = "kechki" kabi mutatsiya bo'ladi.

flowchart LR
  subgraph Eski
    O1["order"] --> W1["waiter: kunduzgi"]
  end
  subgraph Yangi
    O2["order (yangi)"] --> W2["waiter (yangi): kechki"]
  end
  O2 -. "table: 4 — o'sha qiymat" .-> O1

Diagrammada: yangi order va yangi waiter — ikkita yangi obyekt, eski ikkitasi butun qoldi.

4. Massiv state

4.1 Qaysi metod joyida o'zgartiradi

Massivda ham qoida o'sha: state'dagi massivni joyida o'zgartirmang. JavaScript'ning ko'p metodi aynan joyida o'zgartiradi — ularning har biri uchun o'zgartirmaydigan juftligi bor:

Ish joyida (mutatsiya) yangi massiv
qo'shish push, unshift [...arr, x], [x, ...arr]
o'chirish splice, pop, shift filter, toSpliced
o'zgartirish arr[i] = x map, with(i, x)
o'rtaga qo'yish splice(i, 0, x) toSpliced(i, 0, x)
saralash sort, reverse toSorted, toReversed

toSorted, toReversed, toSpliced, with — ES2023 metodlari (Nusxa bilan o'zgartirish). Ular aynan React kabi kodlar uchun qo'shilgan.

4.2 Hammasi bitta misolda

«Bahor» menyu muharriri: qo'shish, o'chirish, narxni o'zgartirish, saralash. Har tugma yangi massiv yasaydi:

jsx
import { useState } from "react";

function App() {
  const [menu, setMenu] = useState([
    { id: 1, name: "Osh", price: 35000 },
    { id: 2, name: "Manti", price: 30000 },
  ]);

  return (
    <>
      <ol>
        {menu.map((d) => (
          <li key={d.id}>
            {d.name} — {d.price}
          </li>
        ))}
      </ol>
      <button
        onClick={() =>
          setMenu([...menu, { id: 3, name: "Somsa", price: 8000 }])
        }
      >
        Somsa
      </button>
      <button
        onClick={() =>
          setMenu(menu.map((d) => ({ ...d, price: d.price * 1.1 })))
        }
      >
        +10%
      </button>
      <button
        onClick={() =>
          setMenu(menu.toSorted((a, b) => a.price - b.price))
        }
      >
        Arzonidan
      </button>
      <button
        onClick={() => setMenu(menu.filter((d) => d.id !== 2))}
      >
        Manti ×
      </button>
    </>
  );
}

Ekranda:

text
Somsa — 8800
Osh — 38500
Somsa+10%ArzonidanManti ×

Blok to'rt tugmani ketma-ket bosdi. Natija oynasida ularni istalgan tartibda bosib ko'ring. Har tugma — bitta qator kod:

  • qo'shish — [...menu, yangi];
  • narxni o'zgartirish — map, har element uchun { ...d, price } — massiv ham, o'zgargan obyektlar ham yangi;
  • saralash — toSorted (sort — menu ning o'zini buzardi);
  • o'chirish — filter.

Bu narxlarda kasr chiqmadi: 35000 * 1.1 — aniq 38500. Lekin ko'paytirish har doim butun son bermaydi (Pul va aniq hisob-kitob). Haqiqiy kassada natijani Math.round bilan yaxlitlang.

4.3 Bitta elementni o'zgartirish: map va tegilmaganlar

Eng ko'p uchraydigan amal — ro'yxatdagi bitta obyektni o'zgartirish:

jsx
setMenu(
  menu.map((d) => (d.id === id ? { ...d, soldOut: true } : d)),
);

Ternary: id mos bo'lsa — yangi obyekt, aks holda — d ning o'zi. Natija: yangi massiv, ichida bitta yangi obyekt va qolganlari o'sha-o'sha. Bu strukturaviy bo'lishish (structural sharing) deyiladi (Immutability chuqur). 1 000 vazifali ro'yxatda bitta katak bosilsa — 1 000 havola nusxalanadi, lekin faqat bitta obyekt yaratiladi. Arzon.

React buning foydasini ham ko'radi. Tegilmagan elementlar o'sha obyekt — keyinchalik memo ular uchun qayta chizishni butunlay o'tkazib yuborishi mumkin.

Tekshirib ko'ring: setMenu(menu.map((d) => { d.price += 1000; return d; })) — yangi massiv yaratildi-ku. Muammo bormi?

Javob

Bor. Massiv yangi, lekin ichidagi obyektlar o'sha — ular joyida o'zgartirildi. Ekran yangilanadi (massiv havolasi yangi), lekin eski state ham buzildi. Eski massivni boshqa joy saqlagan bo'lsa (undo tarixi, boshqa komponent), u ham o'zgarib qoladi. To'g'risi: menu.map((d) => ({ ...d, price: d.price + 1000 })).

5. Sayoz nusxa tuzog'i

Spread faqat birinchi qavatni nusxalaydi (Sayoz va chuqur nusxalash). Bu tuzoqqa olib keladi:

jsx
import { useState } from "react";

const saved = [{ id: 1, dish: "Osh", done: false }];

function App() {
  const [list, setList] = useState(saved);

  function markDone() {
    const copy = [...list];
    copy[0].done = true; // ❌ obyekt o'sha — saved ham o'zgaradi
    setList(copy);
    console.log("saved[0].done:", saved[0].done);
  }

  return (
    <>
      <p>
        {list[0].dish}: {list[0].done ? "tayyor" : "kutilmoqda"}
      </p>
      <button onClick={markDone}>Tayyor deb belgila</button>
    </>
  );
}

Konsolda:

text
saved[0].done: true

Ekranda:

text
Osh: tayyor
Tayyor deb belgila

Ekran to'g'ri — massiv yangi, React qayta chizdi. Lekin saved (masalan, serverdan kelgan asl nusxa) ham buzildi. "Bekor qilish" endi asl holatni topa olmaydi. Tuzatish — o'zgarayotgan obyektni ham nusxalash: list.map((d, i) => (i === 0 ? { ...d, done: true } : d)).

5.1 structuredClone — chuqur nusxa

structuredClone(list) hamma qavatni nusxalaydi (Sayoz va chuqur nusxalash). Keyin nusxani bemalol o'zgartirish mumkin. Lekin u state yangilash uchun kam ishlatiladi:

  • qimmat — o'zgarmagan qismlarni ham nusxalaydi (strukturaviy bo'lishish yo'qoladi);
  • havolalar yo'qoladi — hamma element yangi, React va memo "hamma narsa o'zgardi" deb ko'radi;
  • funksiya va klass nusxalarini ko'chira olmaydi.

Qachon o'rinli: tashqi manbadan kelgan ma'lumotni "o'zimizniki" qilib olishda, bir marta. Har bosishda emas.

5.2 Immer: mutatsiya qilgandek yozish

Chuqur tuzilmada spread'lar zinapoyasi uzun va xatoga moyil. Immer kutubxonasi boshqa yo'l beradi: siz "qoralama" (draft) ustida oddiy mutatsiya yozasiz, Immer esa o'zgarmas natija yasaydi. Biz Node 24 da sinab ko'rdik (npm i immer@11.1.21, 2026-10 holatiga joriy versiya):

js
import { produce } from "immer";

const order = {
  table: 4,
  waiter: { name: "Sardor", shift: "kechki" },
  items: [
    { id: 1, dish: "Osh", count: 2 },
    { id: 2, dish: "Ko'k choy", count: 1 },
  ],
};

const next = produce(order, (draft) => {
  draft.items[1].count += 2; // "mutatsiya" — lekin draft'da
  draft.items.push({ id: 3, dish: "Non", count: 1 });
});

console.log(order.items.length, next.items.length);
console.log(order.items[1].count, next.items[1].count);
console.log(next.items[0] === order.items[0]); // tegilmagan — o'sha
console.log(next.waiter === order.waiter);
console.log(Object.isFrozen(next));

node immer.mjs chiqishi:

text
2 3
1 3
true
true
true

order butun qoldi (2 ta taom, choy 1). next — yangi, lekin tegilmagan items[0] va waiter — o'sha obyektlar: strukturaviy bo'lishish avtomatik. Immer natijani muzlatadi ham (Object.isFrozen — true): keyingi tasodifiy mutatsiya xato beradi. React bilan qulay ishlatish uchun use-immer paketi bor (0.11.0, React 19 ni qo'llaydi) — useImmer hook'i. Bu blok saytda ishga tushmaydi: natija oynasi faqat react paketlarini yuklaydi. Immer'ni o'z loyihangizda sinab ko'ring. Uni reducer bilan birga — Reducer + Immer darsida chuqur ko'ramiz.

Qachon Immer kerak? Tuzilma 3+ qavat chuqur bo'lsa yoki yangilashlar ko'p bo'lsa. Oddiy ro'yxat va formalar uchun spread, map va filter yetadi — vazifalar da Immer yo'q.

6. TypeScript: readonly — mutatsiyani kompilyatsiyada to'xtatish

Mutatsiya jim xato. TypeScript uni ovozli qiladi — readonly bilan (Massiv va tuple turlari, Obyekt turlari):

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

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

menu.push({ id: 2, name: "Manti" });
menu[0].name = "Palov";

export const result = menu;

Xato:

text
TS2339: Property 'push' does not exist on type 'readonly Dish[]'.
TS2540: Cannot assign to 'name' because it is a read-only property.

"readonly Dish[] turida push xususiyati yo'q" va "name — faqat o'qiladigan xususiyat, unga yozib bo'lmaydi". readonly massivda push, sort, splice kabi joyida o'zgartiradigan metodlar umuman yo'q. map, filter, toSorted esa bor — ular yangi massiv qaytaradi. Turlar ish vaqtida yo'qoladi, lekin xato kod hech qachon build'gacha yetmaydi.

7. Vazifalar qadami: amallar.ts va o'zgarmas ro'yxat

State I darsida boshlangan refactor/forma-usestate branch'ini bugun yakunlaymiz. Maqsad — holat.vazifalar o'zgarmas massiv bo'lsin, uni o'zgartiradigan funksiyalar esa yangi massiv qaytarsin.

7.1 v6 da nima edi

v6 da ro'yxat — VazifalarRoyxati klassi (Yakuniy loyiha: vazifalar v2 dan beri). Uning metodlari ichki massivni joyida o'zgartirardi:

ts
qosh(matn: string): Vazifa {
  …
  this.#vazifalar.push(vazifa);
  this.#xabarBer();
}

Ekranni yangilash uchun klassda kuzatuvchi bor edi (obuna, #xabarBer). React'ga o'tganda bu ikki muammo beradi. Birinchisi — massiv o'sha qolib, React o'zgarishni sezmaydi («Nega bu kerak?» bo'limi). Ikkinchisi — server xato qaytarsa, oldingi holatga qaytish uchun avval nusxa olish kerak edi (holat.vazifalar.toJSON()).

7.2 src/amallar.ts — sof funksiyalar

Yangi fayl:

ts
// amallar.ts — vazifalar ro'yxatini o'zgartiruvchi SOF funksiyalar
// (17/#14). Hech biri massivni yoki vazifani joyida o'zgartirmaydi:
// yangi massiv qaytaradi, tegilmagan vazifalar — o'sha
// obyektlarning o'zi (nusxa emas). React o'zgarishni havola
// bo'yicha (Object.is) sezadi: push() yoki v.bajarildi = true dan
// keyin massiv "o'sha-o'sha" — ekran yangilanmaydi. v6 dagi
// VazifalarRoyxati.qosh/almashtir... shu yerga ko'chdi (17/#14)
import { Vazifa } from "./vazifa.ts";
import type { VazifaMalumoti } from "./turlar.ts";

type Vazifalar = readonly VazifaMalumoti[];

/**
 * Yangi id — eng kattasidan keyingisi. Alohida hisoblagich
 * saqlanmaydi: u ro'yxatdan kelib chiqadi (hosilaviy qiymat, 17/#15)
 */
export function keyingiId(vazifalar: Vazifalar): number {
  return vazifalar.reduce((eng, v) => Math.max(eng, v.id), 0) + 1;
}

/** @returns oxiriga yangi vazifa qo'shilgan YANGI massiv */
export function qosh(vazifalar: Vazifalar, matn: string): Vazifalar {
  const yangi = { id: keyingiId(vazifalar), matn, bajarildi: false };
  return [...vazifalar, yangi];
}

/**
 * @returns id li vazifaning bajarildi'si teskarisiga o'zgargan
 * massiv
 */
export function almashtir(vazifalar: Vazifalar, id: number): Vazifalar {
  return vazifalar.map((v) =>
    v.id === id ? { ...v, bajarildi: !v.bajarildi } : v,
  );
}

/**
 * Bo'sh yoki juda uzun matn qabul qilinmaydi — o'sha massivning
 * o'zi qaytadi (havola o'zgarmadi = o'zgarish yo'q)
 */
export function matnniOzgartir(
  vazifalar: Vazifalar,
  id: number,
  matn: string,
): Vazifalar {
  const toza = matn.trim();
  if (toza === "" || toza.length > Vazifa.MAKS_UZUNLIK) {
    return vazifalar;
  }
  return vazifalar.map((v) => (v.id === id ? { ...v, matn: toza } : v));
}

export function ochir(vazifalar: Vazifalar, id: number): Vazifalar {
  return vazifalar.filter((v) => v.id !== id);
}

Izohlar telefon uchun qayta bo'lingan, kod qatorlari kanondagidek. Bugungi darsning hammasi shu 50 qatorda:

  • qosh — [...vazifalar, yangi]: qo'shish.
  • almashtir — map + ternary + { ...v, bajarildi: !v.bajarildi }: bitta elementni o'zgartirish, qolganlari — o'sha obyektlar.
  • matnniOzgartir — noto'g'ri matnda o'sha massivni qaytaradi. Havola o'zgarmadi — "o'zgarish yo'q". Chaqiruvchi buni === bilan bilishi mumkin.
  • ochir — filter.
  • keyingiId — id endi alohida hisoblagichda emas, ro'yxatdan hisoblanadi (Pure komponentlar va State strukturasi g'oyasi: hosilaviy qiymat).
  • readonly VazifaMalumoti[] — «TypeScript: readonly» bo'limidagi himoya: bu funksiyalar ichida push yozilsa, tsc to'xtatadi.

Hech biri React'ni import qilmaydi. Ular — oddiy sof funksiyalar: node:test da build'siz sinaladi.

7.3 holat.ts: massiv va tinglovchi

holat.vazifalar endi klass emas, readonly massiv. O'zgartiruvchilar amallar.ts ni chaqiradi va yangi massivni yozadi:

diff
 export type Holat = {
-  vazifalar: VazifalarRoyxati;
+  // readonly massiv: push/splice ni tsc to'xtatadi — faqat
+  // almashtirish
+  vazifalar: readonly VazifaMalumoti[];
 …
+function vazifalarniYoz(yangi: readonly VazifaMalumoti[]): void {
+  holat.vazifalar = yangi;
+  tinglovchi();
+}
 …
 export function vazifaniAlmashtir(id: number): void {
-  holat.vazifalar.almashtir(id);
+  vazifalarniYoz(almashtir(holat.vazifalar, id));
 }

VazifalarRoyxati klassi qoldi, lekin endi faqat o'qiydi: filtr, statistika, teglar. Uni kerak joyda royxatniOl() massivdan yasaydi. Klassning obuna va mutatorlari olib tashlandi — xabarni endi holat.ts dagi bitta tinglovchi beradi.

7.4 asosiy.ts: nusxa endi kerak emas

Eng chiroyli o'zgarish — server bilan ishlashda. Server rad etsa, ro'yxat oldingi holatga qaytadi. v6 da buning uchun nusxa olinardi:

diff
-  // Nusxa — qaytarish uchun: mahalliy() ro'yxatni joyida
-  // o'zgartiradi
-  const oldingi = holat.vazifalar.toJSON();
+  // Qaytarish uchun eski ro'yxat. Nusxa shart emas (17/#14):
+  // mahalliy() uni o'zgartirmaydi, yangi massiv yasaydi — eskisi
+  // butun qoladi
+  const oldingi = holat.vazifalar;
   mahalliy();

oldingi — shunchaki havola. Hech kim eski massivni o'zgartirmaydi — u "arxivdagi chek" kabi butun qoladi. «Nega aynan havola» bo'limidagi va'da qilingan foyda — mana shu.

7.5 Formani sahifaga ulash

State I da yozilgan YangiVazifaForma endi sahifaga chiqadi. index.html dagi <form id="yangi-forma"> o'rnida bo'sh joy qoladi:

diff
-    <form id="yangi-forma" novalidate>
-      …
-    </form>
+    <!-- Yangi vazifa formasi — React (17/#12) -->
+    <div id="yangi-forma-joyi"></div>

render.tsx dagi formaniChiz() uni uchinchi React oroli sifatida chizadi. U flushSync bilan chiziladi: forma birinchi kadrdayoq bo'ladi va ro'yxat pastga "sakramaydi" (CLS 0). Shuningdek asosiy.ts dan formaning beshta vanilla tinglovchisi (submit, input, keydown, takliflar click va keydown — 50 qatorga yaqin) va xatoniKorsat/xatoniTozala o'chdi. O'rniga — vazifaniQosh(matn): QoshishNatijasi.

7.6 Testlar avval yiqildi

Bu safar testlar avval yiqildi — va bu yaxshi belgi. npm test (holat massivga o'tgandan keyin, testlar hali eski):

text
ℹ tests 146
ℹ pass 132
ℹ fail 14

14 ta eski test VazifalarRoyxati ning mutatorlarini (qosh, obuna…) va o'zgaruvchan holat ni sinardi. Ular yangi kod bilan endi mos emas — xulq o'zgardi va testlar buni ko'rsatdi. Eski testlarning keraksizi olib tashlandi, qolgani yangi API'ga moslandi. Yangi fayl — tekshiruv/amallar.test.js, 7 ta test. Uning boshidagi g'oya:

js
// Muzlatilgan: biror amal joyida o'zgartirsa — TypeError
// (strict mode)
const boshlangich = Object.freeze([
  Object.freeze({ id: 1, matn: "Non olish", bajarildi: false }),
  …
]);

Test ma'lumoti Object.freeze bilan muzlatilgan. Biror funksiya ichida push yoki v.bajarildi = … qolib ketsa — TypeError. Muzlatish — o'zgarmaslikning ish vaqtidagi qorovuli, readonly esa kompilyatsiyadagisi. Testlardan biri strukturaviy bo'lishishni ham tekshiradi:

js
test("almashtir: faqat o'sha vazifa yangi obyekt", () => {
  const yangi = almashtir(boshlangich, 1);
  assert.equal(yangi[0].bajarildi, true);
  assert.equal(boshlangich[0].bajarildi, false);
  assert.notEqual(yangi[0], boshlangich[0]);
  assert.equal(yangi[1], boshlangich[1]);
});

assert.equal(yangi[1], boshlangich[1]) — === bilan: tegilmagan vazifa o'sha obyekt.

Natija (node --test, oxirgi qatorlar):

text
ℹ tests 149
ℹ suites 37
ℹ pass 149
ℹ fail 0

Vitest — 14/14 (State I dagi forma testlari bilan). lint, format:check, tip — toza.

7.7 Xulq farqi va ikki o'lchov

Id. v6 da o'chirilgan eng katta id qayta berilmasdi — hisoblagich alohida edi. Endi id ro'yxatdan hisoblanadi: 4-vazifa o'chirilsa, keyingi yangisi yana 4 oladi. Bu hujjatlangan farq: server id'ni baribir o'zi beradi, TEXNIK-QARZ.md ga yozildi. Test ham bor: "id hisoblagichda emas, ro'yxatdan: o'chirilgan eng katta id qaytadi".

Brauzer tekshiruvi. Forma React'ga o'tgach, 159 tekshiruvning bir qismi yiqildi. Ssenariy yozishni taqlid qilardi: maydon.value = "…" va new Event("input"). Bunday sun'iy hodisa ko'tarilmaydi (bubbles: false). React esa hodisalarni ildizda ushlaydi — Hodisalar darsidagi "React ham delegatsiya qiladi". Brauzerning haqiqiy input hodisasi doim ko'tariladi. Shuning uchun ssenariy tuzatildi: new Event("input", { bubbles: true }), to'rt joyda. v6 build'i ham shu ssenariy bilan 159/159 o'tadi. Ya'ni tekshiruv qattiqlashmadi, haqiqiy brauzerga yaqinlashdi. Natija: 159/159.

knip. npm run knip bitta narsa topadi:

text
Unused exports (1)
tegTakliflari  function  src/render.tsx:129:17

tegTakliflari endi faqat render.tsx ning ichida (formaniChiz) ishlatiladi — eksport ortiqcha. Kanonda u hozircha qoldirilgan: State'ni ko'tarish darsida funksiya teglar.ts ga ko'chadi va eksport haqiqatan kerak bo'ladi. Siz export ni hozir olib tashlasangiz ham bo'ladi.

7.8 Commit

text
refactor: yangi vazifa formasi — useState; ro'yxat o'zgarmas massiv

YangiVazifaForma (xato va takliflar — state). amallar.ts — sof
qosh/almashtir/ochir/matnniOzgartir; holat.vazifalar — readonly
VazifaMalumoti[], VazifalarRoyxati faqat o'qiydi.

Diff: 16 fayl, +579 −326. Uch dars — bitta branch, bitta PR, gh pr merge --merge.

8. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
bosildi, ekran yangilanmadi, xato yo'q push/obj.x = va o'sha havola [...arr, x], { ...obj, x }
ekran yangilandi, lekin asl ma'lumot buzildi sayoz nusxadagi obyektni o'zgartirish o'zgargan obyektni ham spread
ichma-ich maydon yangilanmadi bitta qavat nusxalangan har qavat: { ...a, b: { ...a.b, c } }
boshqa joydagi tartib buzildi sort/reverse toSorted/toReversed
Property 'push' does not exist on type 'readonly …' readonly himoyasi yangi massiv yasang
hamma qator qayta chiziladi structuredClone har bosishda map + tegilmaganini qaytaring

9. Mashqlar

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

1-mashq (oson): qaysi biri ishlaydi?

setOrder va setList chaqiruvlaridan qaysilari ekranni yangilaydi va asl ma'lumotni buzmaydi?

js
// A
order.count++; setOrder(order);
// B
setOrder({ ...order, count: order.count + 1 });
// C
setList([...list, "Non"]);
// D
setList(list.sort());
// E
setList(list.toSorted());
// F
const copy = [...list]; copy[0].done = true; setList(copy);
  • To'g'rilari (harflar, alifbo tartibida):
Yechim

B, C, E. A — o'sha obyekt, ekran yangilanmaydi. D — sort list ni joyida o'zgartiradi va o'sha massivni qaytaradi: havola o'zgarmadi, ekran yangilanmaydi. F — massiv yangi, ekran yangilanadi, lekin copy[0] — asl obyekt: u buzildi.

2-mashq (o'rta): savatni boshqarish

Cart komponenti: state — items, massiv: [{ id: 1, dish: "Osh", count: 1 }, { id: 2, dish: "Choy", count: 2 }]. Har qatorda "+", "−" va "×" tugmalari (aria-label: "Osh +", "Osh −", "Osh ×"). Talablar:

  • "+" — count ni 1 ga oshiradi (map);
  • "−" — kamaytiradi; count 1 bo'lsa, qator o'chadi (map, keyin filter);
  • "×" — o'chiradi (filter);
  • "Non +1" — oxiriga { id: 3, dish: "Non", count: 1 };
  • pastda — jami dona (reduce, state emas).

Blok: "Osh +", "Choy −", "Choy −", "Non +1". Ekranda:

text
Osh × 2 +−×
Non × 1 +−×
Non +1
Jami: 3 dona
Yechim
jsx
import { useState } from "react";

function Cart() {
  const [items, setItems] = useState([
    { id: 1, dish: "Osh", count: 1 },
    { id: 2, dish: "Choy", count: 2 },
  ]);

  function change(id, delta) {
    const changed = items.map((i) =>
      i.id === id ? { ...i, count: i.count + delta } : i,
    );
    setItems(changed.filter((i) => i.count > 0));
  }

  const total = items.reduce((sum, i) => sum + i.count, 0);

  return (
    <>
      <ul>
        {items.map((i) => (
          <li key={i.id}>
            {i.dish} × {i.count}{" "}
            <button
              aria-label={`${i.dish} +`}
              onClick={() => change(i.id, 1)}
            >
              +
            </button>
            <button
              aria-label={`${i.dish} −`}
              onClick={() => change(i.id, -1)}
            >
              −
            </button>
            <button
              aria-label={`${i.dish} ×`}
              onClick={() =>
                setItems(items.filter((x) => x.id !== i.id))
              }
            >
              ×
            </button>
          </li>
        ))}
      </ul>
      <button
        onClick={() =>
          setItems([...items, { id: 3, dish: "Non", count: 1 }])
        }
      >
        Non +1
      </button>
      <p>Jami: {total} dona</p>
    </>
  );
}

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

Ekranda:

text
Osh × 2 +−×
Non × 1 +−×
Non +1
Jami: 3 dona

"−" ikki amalni zanjir qiladi: map (kamaytirish) va filter (nolga tushganini olib tashlash) — ikkalasi ham yangi massiv. total — reduce bilan har chizishda hisoblanadi (reduce).

3-mashq (qiyin): ichma-ich buyurtma (TypeScript)

Stol buyurtmasi, .tsx da. Tur:

tsx
type Item = {
  readonly id: number;
  readonly dish: string;
  readonly count: number;
};
type Order = {
  readonly table: number;
  readonly guest: { readonly name: string; readonly phone: string };
  readonly items: readonly Item[];
};

Yozing:

  • renameGuest(order, name) — mehmon ismini o'zgartiradi (ikki qavat);
  • addItem(order, dish) — items oxiriga yangi taom, id — eng kattasidan +1;
  • increment(order, id) — bitta taomning count i +1;
  • uchalasi ham sof (yangi Order qaytaradi, readonly buni majburlaydi);
  • App — useState<Order>, uch tugma va ekranda: 5-stol, Malika: Osh × 3, Non × 1.

Blok: "Ism", "Osh +1", "Osh +1", "Non". Boshlang'ich: 5-stol, mehmon "Mehmon", Osh × 1.

Yechim
tsx
import { useState } from "react";

type Item = {
  readonly id: number;
  readonly dish: string;
  readonly count: number;
};
type Order = {
  readonly table: number;
  readonly guest: { readonly name: string; readonly phone: string };
  readonly items: readonly Item[];
};

function renameGuest(order: Order, name: string): Order {
  return { ...order, guest: { ...order.guest, name } };
}

function addItem(order: Order, dish: string): Order {
  const ids = order.items.map((i) => i.id);
  const maxId = Math.max(0, ...ids);
  const item = { id: maxId + 1, dish, count: 1 };
  return { ...order, items: [...order.items, item] };
}

function increment(order: Order, id: number): Order {
  return {
    ...order,
    items: order.items.map((i) =>
      i.id === id ? { ...i, count: i.count + 1 } : i,
    ),
  };
}

const start: Order = {
  table: 5,
  guest: { name: "Mehmon", phone: "+998 90 000 00 00" },
  items: [{ id: 1, dish: "Osh", count: 1 }],
};

export default function App() {
  const [order, setOrder] = useState<Order>(start);
  const lines = order.items.map((i) => `${i.dish} × ${i.count}`);
  return (
    <>
      <p>
        {order.table}-stol, {order.guest.name}: {lines.join(", ")}
      </p>
      <button onClick={() => setOrder(renameGuest(order, "Malika"))}>
        Ism
      </button>
      <button onClick={() => setOrder(increment(order, 1))}>
        Osh +1
      </button>
      <button onClick={() => setOrder(addItem(order, "Non"))}>
        Non
      </button>
    </>
  );
}

Ekranda:

text
5-stol, Malika: Osh × 3, Non × 1
IsmOsh +1Non

Funksiyalar komponentdan tashqarida va sof. Ularni vazifalar dagi amallar.ts kabi alohida faylga chiqarib, node:test bilan sinash mumkin. readonly tufayli order.items.push(…) yoki order.guest.name = … yozib bo'lmaydi — tsc to'xtatadi. start obyekti esa hech qachon o'zgarmaydi: undo kerak bo'lsa, u tayyor.

4-mashq: Vazifalar qadami

«Vazifalar qadami» bo'limidagi o'zgarishlarni kiriting: amallar.ts, holat.ts, royxat.ts va vazifa.ts (mutatorlar olib tashlanadi), render.tsx (formaniChiz, royxatniOl), asosiy.ts, index.html, testlar. Avval faqat holat.ts ni o'zgartirib npm test ni ishga tushiring — nechta test yiqildi? Keyin testlarni yangilang va npm run check toza bo'lsin. Commit — bo'limdagi xabar bilan.

Yechim

Tekshiruv ro'yxati:

  • O'tish paytida yiqilgan testlar — kutilgan: ular eski, o'zgaruvchan API'ni sinaydi. Har yiqilgan testni o'qing — "bu xulq kerakmi?" — keyin yangisiga ko'chiring yoki olib tashlang.
  • node --test — 149/149, amallar.test.js dagi muzlatilgan ro'yxat bilan. TypeError: Cannot add property … chiqsa — biror funksiya joyida o'zgartiryapti.
  • Vitest — 14/14.
  • npm run tip — holat.vazifalar.push(…) qolgan bo'lsa, TS2339 beradi.
  • Brauzerda: vazifa qo'shing, belgilang, o'chiring; internetni o'chirib o'chirishga urining — ro'yxat oldingi holatiga qaytishi kerak (oldingi — havola).

Yakuniy kod — refactor: yangi vazifa formasi — useState; ro'yxat o'zgarmas massiv commit'i.

10. Real ishda

  • Immutable yangilash — React, Redux, Zustand va boshqa holat kutubxonalarining umumiy qoidasi. Redux Toolkit ichida Immer ishlatadi.
  • Kod ko'rigida eng ko'p ushlanadigan React xatolaridan biri — state'ni joyida o'zgartirish. TypeScript'da readonly va ESLint qoidalari buni oldindan to'xtatadi.
  • Undo/redo, "optimistik" yangilash (server javobini kutmasdan ekranni o'zgartirish va xatoda qaytarish) — o'zgarmas ma'lumot bilan oson: eski holat — shunchaki havola.
  • Intervyu savollari: "Nega state'ni mutatsiya qilib bo'lmaydi?", "sort va toSorted farqi?", "Sayoz va chuqur nusxa?", "Immer qanday ishlaydi?".

Xulosa

  • React state o'zgarishini havola (Object.is) bo'yicha sezadi: joyida o'zgartirilgan obyekt yoki massiv — "o'zgarish yo'q".
  • Obyekt: { ...obj, maydon }; ichma-ich — har qavat nusxalanadi.
  • Massiv: qo'shish — spread, o'chirish — filter, o'zgartirish — map, saralash — toSorted, almashtirish — with.
  • Tegilmagan elementlar o'sha obyekt qoladi — strukturaviy bo'lishish arzon va foydali; sayoz nusxadagi obyektni o'zgartirmang.
  • structuredClone — kam holatda; chuqur tuzilmalar uchun Immer; TypeScript'da readonly mutatsiyani kompilyatsiyada to'xtatadi.
  • vazifalar da amallar.ts — sof yangilash funksiyalari, holat.vazifalar — readonly massiv, qaytarish uchun nusxa kerak emas; 149 + 14 test, 159/159.

Keyingi dars: State strukturasini loyihalash — state'da nima saqlash, nimani hisoblash kerakligini va vazifalar da bitta App ildizini yig'ishni o'rganamiz.

Manbalar

  • React: "Updating Objects in State", "Updating Arrays in State" — react.dev/learn
  • MDN: Array.prototype.toSorted, with, toSpliced — developer.mozilla.org
  • Immer — immerjs.github.io/immer; use-immer — github.com/immerjs/use-immer
  • MDN: structuredClone() — developer.mozilla.org/docs/Web/API/Window/structuredClone
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
State III: obyekt va massivlarni immutable yangilash — spread, map, filter va Immer — IlmHamroh