IlmHamroh
JavaScript Full-stack/17-qism. React asoslari40/45-dars20 daqiqa
Mundarija (22)

React reducer + Immer va holat mashinasi: ichma-ich holat va "imkonsiz holatlar"siz UI

Qisqacha: Chuqur ichma-ich holatni spread bilan yangilash uzun va xatoga moyil. Immer'ning produce i (React'da — use-immer paketidagi useImmerReducer) reducer'da qoralamani "joyida o'zgartirgandek" yozishga imkon beradi, natija esa baribir yangi, muzlatilgan obyekt. Ikkinchi g'oya — holat mashinasi: isLoading, isError, isSuccess kabi bir nechta boolean o'rniga bitta status maydoni ("kiritish" | "yuborilmoqda" | "tasdiqlandi" | "xato"). Shunda "ham yuklanmoqda, ham xato" kabi imkonsiz holat yozib bo'lmaydigan bo'ladi. Reducer esa har holatda faqat ruxsat etilgan hodisalarni qabul qiladi.

Bu darsda

  • Ichma-ich holatni spread va Immer bilan yangilashni taqqoslaysiz.
  • produce ni reducer'da va useImmerReducer ni komponentda ishlatasiz, Immer xatolarini taniysiz.
  • Boolean bayroqlar keltirib chiqaradigan "imkonsiz holatlar"ni sanab, ularni status bilan yo'qotasiz.
  • Holat mashinasini jadval va diagramma sifatida chizib, reducer'ga aylantirasiz.
  • XState va Zag qachon kerakligini bilasiz.

Oldin bilishingiz kerak: useReducer, Immutability chuqur (Immer), State III: immutable yangilash, Discriminated union.

1. Nega bu kerak?

«Bahor» endi ikki filialda: Chilonzor va Yunusobod. Kassa ekranida har filialning buyurtmalari, har buyurtmada taomlar. Sardor reducer'da "Chilonzordagi 1-buyurtmaga yana bitta osh" amalini yozdi va to'rt qavatli spread hosil bo'ldi. Ertasiga bitta qavsni noto'g'ri yopdi — Yunusobodning buyurtmalari yo'qolib qoldi.

Ikkinchi muammo bron formasida. Sardor uchta state yozdi: isLoading, isError, isSuccess. Bir kuni Jasur aka ekranda bir vaqtning o'zida "Yuborilmoqda…" va "Xato yuz berdi" yozuvlarini ko'rdi. Qayta urinish tugmasi isLoading ni yoqdi-yu, isError ni o'chirishni unutdi.

Ikkala muammoning ildizi bir: holat shakli kodni xatoga undaydi. Bugun ikkalasini ham tuzatamiz. Birinchisini — Immer bilan, ikkinchisini — holat mashinasi tafakkuri bilan.

Hayotiy o'xshatish: svetofor. Uchta chiroq bor, lekin ular uchta mustaqil kalit emas. Svetofor faqat bir nechta aniq holatda bo'ladi (qizil, sariq, yashil) va ular orasida aniq tartib bilan o'tadi. "Qizil va yashil birga yongan" holat — yo'q, chunki u qurilmaning o'zida mumkin emas. Holat mashinasi — shunday svetofor.

2. Immer bilan ichma-ich holat

2.1 Spread piramidasi

Immutability chuqur darsida Immer'ning produce funksiyasi bilan tanishgan edik: "joyida yozing — o'zgarmas oling". Endi uni reducer'da ishlatamiz. Avval muammoni ko'ramiz — bir xil o'zgarish ikki usulda:

js
import { produce } from "immer";

const state = {
  branches: [
    { id: "chilonzor", orders: [{ id: 1, dish: "Osh", qty: 1 }] },
    { id: "yunusobod", orders: [{ id: 2, dish: "Manti", qty: 3 }] },
  ],
};

// spread bilan: Chilonzordagi 1-buyurtma soni +1
const bySpread = {
  ...state,
  branches: state.branches.map((b) =>
    b.id !== "chilonzor"
      ? b
      : {
          ...b,
          orders: b.orders.map((o) =>
            o.id === 1 ? { ...o, qty: o.qty + 1 } : o,
          ),
        },
  ),
};

// Immer bilan: xuddi o'sha o'zgarish
const byImmer = produce(state, (draft) => {
  const branch = draft.branches.find((b) => b.id === "chilonzor");
  branch.orders.find((o) => o.id === 1).qty += 1;
});

console.log(JSON.stringify(bySpread) === JSON.stringify(byImmer));
const before = state.branches[0].orders[0].qty;
console.log(before, byImmer.branches[0].orders[0].qty);
console.log(byImmer.branches[1] === state.branches[1]);
const order = byImmer.branches[0].orders[0];
console.log(Object.isFrozen(byImmer), Object.isFrozen(order));
console.log(produce(state, () => {}) === state);

Bu blok tashqi paket (immer 11.1.21) ishlatadi — saytda «Ishga tushir» yo'q. Uni toza papkada (npm i immer@11.1.21) Node 24 da ishga tushirdik:

text
true
1 2
true
true true
true

Har qatorning ma'nosi:

  1. true — ikki usul bir xil natija berdi.
  2. 1 2 — asl holat o'zgarmadi (1), yangisida 2.
  3. true — Yunusobod filiali yangi holatda o'sha obyekt: Immer tegilmagan qismni nusxalamadi — State III dagi strukturaviy bo'lishish (structural sharing). React uchun bu muhim: shu filialni ko'rsatadigan komponent "o'zgarish yo'q" deb biladi.
  4. true true — natija muzlatilgan (freeze): keyin uni tasodifan o'zgartirib bo'lmaydi.
  5. true — retsept hech narsani o'zgartirmasa, asl holatning o'zi qaytadi — React qayta chizmaydi.

2.2 Reducer'ni produce bilan o'rash

produce ga faqat retsept bersangiz, u reducer qaytaradi (bunday shakl "curried" deyiladi — argumentlarni bo'lib olish):

js
import { produce } from "immer";

const cartReducer = produce((draft, action) => {
  switch (action.type) {
    case "added": {
      const item = draft.find((i) => i.name === action.name);
      if (item) item.qty += 1;
      else draft.push({ name: action.name, qty: 1 });
      break;
    }
    case "removed":
      return draft.filter((i) => i.name !== action.name);
    default:
      throw new TypeError(`Noma'lum amal: ${action.type}`);
  }
});

Uch narsaga e'tibor bering:

  • added — qoralamani "o'zgartiradi" va hech narsa qaytarmaydi (break). Immer o'zgarishlarni yig'ib, yangi holat yasaydi.
  • removed — yangi massivni qaytaradi. Butun holatni almashtirish kerak bo'lsa, shu usul.
  • Ikkalasini birga qilish mumkin emas (pastda).

useReducer darsidagi push xatosi bu yerda xato emas: draft — asl holat emas, uning Proxy "qoralamasi".

2.3 useImmerReducer

React'da buning tayyor hook'i bor — use-immer paketi (0.11.0, peer: immer >=8.0.0, react ^16.8.0 … ^19.0.0). Paket kodi juda qisqa. useImmerReducer — bor-yo'g'i useReducer(useMemo(() => produce(reducer), [reducer]), …). Ya'ni yuqoridagi o'rashni o'zi qiladi.

jsx
import { useImmerReducer } from "use-immer";

function cartReducer(draft, action) {
  switch (action.type) {
    case "added": {
      const item = draft.find((i) => i.name === action.name);
      if (item) item.qty += 1;
      else draft.push({ name: action.name, qty: 1 });
      break;
    }
    case "removed":
      return draft.filter((i) => i.name !== action.name);
    default:
      throw new TypeError(`Noma'lum amal: ${action.type}`);
  }
}

export function Cart() {
  const [cart, dispatch] = useImmerReducer(cartReducer, []);
  const text = cart.map((i) => `${i.name}×${i.qty}`).join(", ");
  const add = () => dispatch({ type: "added", name: "Osh" });
  const remove = () => dispatch({ type: "removed", name: "Osh" });
  return (
    <div>
      <p>{text || "bo'sh"}</p>
      <button onClick={add}>Osh</button>
      <button onClick={remove}>Olib tashlash</button>
    </div>
  );
}

Saytdagi «Ishga tushir» faqat react ni yuklaydi, shuning uchun bu blokda tugma yo'q. O'z loyihangizda npm i immer@11.1.21 use-immer@0.11.0 qiling. Biz komponentni React 19.3.0, use-immer 0.11.0, immer 11.1.21 bilan jsdom'da chizdik: «Osh», «Osh», «Olib tashlash» bosildi. <p> matni har qadamda:

text
bo'sh
Osh×1
Osh×2
bo'sh

2.4 Immer tuzoqlari

Immer'ning o'z qoidalari bor. Ularni buzsangiz, aniq xato oladi (hammasi Node 24 + immer 11.1.21 da olingan):

O'zgartirish va qaytarish birga. Strelkali funksiyada figurali qavssiz yozilsa, tasodifan bo'ladi:

text
[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft.

"Immer retsepti yangi qiymat qaytardi va qoralamani o'zgartirdi. Yo u, yo bu." (d) => d.items.push("Manti") — push uzunlikni qaytaradi, Immer uni "yangi qiymat" deb oladi. Qavs qo'ying: (d) => { d.items.push("Manti"); }.

Qoralamani tashqarida saqlash. produce tugagach, qoralama "bekor qilinadi" — Proxy o'chiriladi. Uni o'zgaruvchiga saqlab, keyin ishlatsangiz:

text
TypeError: Cannot perform 'get' on a proxy that has been revoked

"Bekor qilingan proxy ustida get bajarib bo'lmaydi." Qoralama faqat retsept ichida yashaydi. setTimeout, await yoki effektdan unga murojaat qilmang.

Natijani o'zgartirish. Immer natijani muzlatadi:

text
TypeError: Cannot add property 2, object is not extensible

"2-xususiyatni qo'shib bo'lmaydi, obyekt kengaytirilmaydi." Bu — himoya: natijaga push qilish xatosi darhol ko'rinadi, ekran jim eskirib qolmaydi.

Tekshirib ko'ring: Savatda ikki daraja bor: cart.items[i].qty. Immer kerakmi?

Javob

Shart emas. Ikki darajani spread bilan yozish hali o'qiladi: items.map((i) => i.name === n ? { ...i, qty: i.qty + 1 } : i). Immer — qo'shimcha paket va qo'shimcha qoidalar. U holat uch-to'rt daraja chuqur bo'lganda yoki bir amal ko'p joyni o'zgartirganda o'zini oqlaydi. Holatni tekisroq qilish (State strukturasi) ham ko'pincha Immer'siz yechim beradi.

3. Imkonsiz holatlar

3.1 Boolean'lar bilan bron formasi

State strukturasi darsida "imkonsiz holatlar" bilan tanishgan edik: uch boolean — 8 kombinatsiya, ma'nolisi to'rtta. Endi ular reducer bilan qanday yo'qotilishini ko'ramiz. Sardorning bron formasi — uchta mustaqil bayroq:

jsx
import { useState } from "react";

function BookingForm() {
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);
  const [isSuccess, setIsSuccess] = useState(false);

  function submit() {
    setIsLoading(true);
    setTimeout(() => {          // server "xato" qaytardi
      setIsLoading(false);
      setIsError(true);
    }, 50);
  }
  function retry() {
    setIsLoading(true);         // ❌ isError ni o'chirish unutildi
  }

  return (
    <div>
      {isLoading && <p>Yuborilmoqda…</p>}
      {isError && <p>Xato yuz berdi</p>}
      {isSuccess && <p>Stol bron qilindi!</p>}
      <button onClick={submit}>Yuborish</button>{" "}
      <button onClick={retry}>Qayta urinish</button>
    </div>
  );
}

export default BookingForm;

Ekranda:

text
Yuborilmoqda…
Xato yuz berdi
Yuborish Qayta urinish

Jasur aka ko'rgan ekran — aynan shu. Uchta boolean — 2³ = 8 ta kombinatsiya. Ulardan nechtasi ma'noli?

isLoading isError isSuccess Ma'nosi
false false false kiritish
true false false yuborilmoqda
false true false xato
false false true tasdiqlandi
true true false yuklanmoqda va xato
true false true yuklanmoqda va tasdiqlangan
false true true xato va tasdiqlangan
true true true hammasi birga

8 tadan 4 tasi imkonsiz holat — haqiqatda bo'lmasligi kerak, lekin kod ularni yozishga ruxsat beradi. Har setX chaqiruvida dasturchi qolgan ikkitasini to'g'ri qo'yishni eslab turishi kerak. To'rtinchi bayroq qo'shilsa — 16 ta kombinatsiya, ulardan 11 tasi imkonsiz.

3.2 Bitta status

Yechim — bayroqlar o'rniga bitta maydon, u faqat ruxsat etilgan qiymatlardan birini oladi. TypeScript'da bu discriminated union (Discriminated union). Bundan ham yaxshisi: har holatga faqat o'ziga tegishli ma'lumot biriktiriladi:

tsx
type Booking =
  | { status: "kiritish" }
  | { status: "yuborilmoqda" }
  | { status: "tasdiqlandi"; bookingId: string }
  | { status: "xato"; message: string };

Endi bron raqami faqat "tasdiqlandi" holatida bor, xato matni — faqat "xato" da. TypeScript buni kuzatadi:

tsx
type Booking =
  | { status: "kiritish" }
  | { status: "yuborilmoqda" }
  | { status: "tasdiqlandi"; bookingId: string }
  | { status: "xato"; message: string };

function Message({ booking }: { booking: Booking }) {
  if (booking.status === "xato") {
    return <p>Bron raqami: {booking.bookingId}</p>;
  }
  return null;
}

export default Message;

Xato:

text
TS2339: Property 'bookingId' does not exist on type '{ status: "xato"; message: string; }'.

"Bu turda bookingId yo'q." if ichida TypeScript booking ni "xato" variantiga toraytirdi. Unda bron raqami yo'q — xato bo'lgan bronning raqami ham bo'lmaydi. Imkonsiz holatni yaratishga ham urinib ko'ramiz:

tsx
type Booking =
  | { status: "kiritish" }
  | { status: "yuborilmoqda" }
  | { status: "tasdiqlandi"; bookingId: string }
  | { status: "xato"; message: string };

const booking: Booking = {
  status: "yuborilmoqda",
  message: "Tarmoq xatosi",
};

Xato:

text
TS2353: Object literal may only specify known properties, and 'message' does not exist in type '{ status: "yuborilmoqda"; }'.

"Obyekt literalida faqat ma'lum xususiyatlar bo'ladi; 'yuborilmoqda' turida message yo'q." "Yuborilmoqda va xato" — turning o'zida yozib bo'lmaydi.

vazifalar da bunday tur allaqachon bor: TarmoqHolati = "yuklanmoqda" | "tayyor" | "xato" | "oflayn". Aloqa yozuvi to'rt boolean emas, bitta holatdan chiziladi. Bu tasodif emas — ilova boshidan shu tafakkur bilan loyihalangan.

4. Holat mashinasi

4.1 Holatlar va o'tishlar

Holat mashinasi (finite state machine, "chekli holatlar mashinasi") — tizim faqat sanab chiqilgan holatlardan birida bo'ladi va bir holatdan boshqasiga faqat ruxsat etilgan hodisa bilan o'tadi. Bron formasi uchun:

stateDiagram-v2
  [*] --> kiritish
  kiritish --> yuborilmoqda: YUBOR
  yuborilmoqda --> tasdiqlandi: TASDIQ
  yuborilmoqda --> xato: XATO
  xato --> yuborilmoqda: YUBOR
  tasdiqlandi --> [*]

Diagrammada strelkasi yo'q narsa — mumkin emas. "Yuborilmoqda" holatida YUBOR yo'q, demak tugmani ikki marta bosish ikkinchi so'rovni yubormaydi. "Tasdiqlandi" dan chiqish yo'q.

Xuddi shu mashina jadval ko'rinishida — reducer'ning shartnomasi:

Holat \ Hodisa YUBOR TASDIQ XATO
kiritish → yuborilmoqda — —
yuborilmoqda — → tasdiqlandi → xato
xato → yuborilmoqda — —
tasdiqlandi — — —

"—" — hodisa e'tiborsiz qoldiriladi: holat o'zgarmaydi.

4.2 Reducer — holat mashinasi

Jadvalni reducer'ga aylantiramiz. Avval holat bo'yicha, keyin hodisa bo'yicha tanlaymiz:

jsx
import { useReducer } from "react";

function bookingReducer(state, event) {
  console.log(state.status, "+", event.type);
  switch (state.status) {
    case "kiritish":
    case "xato":
      if (event.type === "YUBOR") return { status: "yuborilmoqda" };
      return state;
    case "yuborilmoqda":
      if (event.type === "TASDIQ") {
        return { status: "tasdiqlandi", bookingId: event.bookingId };
      }
      if (event.type === "XATO") {
        return { status: "xato", message: event.message };
      }
      return state;
    case "tasdiqlandi":
      return state;
    default:
      throw new TypeError(`Noma'lum holat: ${state.status}`);
  }
}

const SUBMIT = { type: "YUBOR" };
// server javoblari o'rnida
const SERVER_ERROR = { type: "XATO", message: "Tarmoq xatosi" };
const SERVER_OK = { type: "TASDIQ", bookingId: "B-17" };

function BookingForm() {
  const [booking, send] = useReducer(bookingReducer, {
    status: "kiritish",
  });
  return (
    <div>
      <p>Holat: {booking.status}</p>
      {booking.status === "xato" && <p>{booking.message}</p>}
      {booking.status === "tasdiqlandi" && (
        <p>Bron raqami: {booking.bookingId}</p>
      )}
      <button onClick={() => send(SUBMIT)}>Yubor</button>{" "}
      <button onClick={() => send(SERVER_ERROR)}>Xato</button>{" "}
      <button onClick={() => send(SERVER_OK)}>OK</button>
    </div>
  );
}

export default BookingForm;

Konsolda:

text
kiritish + YUBOR
yuborilmoqda + YUBOR
yuborilmoqda + XATO
xato + YUBOR
yuborilmoqda + TASDIQ

Ekranda:

text
Holat: tasdiqlandi
Bron raqami: B-17
Yubor Xato OK

«Xato» va «OK» tugmalari — server javobining o'rnida (haqiqiy so'rovni effektlar darsida yozgan edik). Amal → holat jadvali:

Qadam Hodisa Holat oldin Holat keyin
1 YUBOR kiritish yuborilmoqda
2 YUBOR (ikkinchi bosish) yuborilmoqda yuborilmoqda — e'tiborsiz
3 XATO yuborilmoqda xato ("Tarmoq xatosi")
4 YUBOR xato yuborilmoqda — xato matni o'z-o'zidan yo'qoldi
5 TASDIQ yuborilmoqda tasdiqlandi ("B-17")

Ikkinchi qadamga qarang: tugma ikki marta bosildi, lekin ikkinchi so'rov "yuborilmadi" — mashina buni rad etdi. To'rtinchi qadamda xato matni alohida tozalanmadi. Yangi holat obyektida u yo'q, shuning uchun "xato va yuborilmoqda" bo'lishi mumkin emas. Boolean'lardagi xatoni yozishning iloji qolmadi.

Maslahat: Hodisa nomlari bu yerda katta harf bilan (YUBOR) — holat mashinasi kutubxonalarida shunday an'ana. useReducer darsidagi "o'tgan zamon" (added) bilan ziddiyat yo'q: ikkalasi ham "nima bo'ldi" degan xabar, faqat yozilish uslubi boshqa.

Tekshirib ko'ring: "Tasdiqlandi" holatidan keyin foydalanuvchi yana bron qilmoqchi. Mashinaga nima qo'shasiz?

Javob

Yangi o'tish: tasdiqlandi --YANGI--> kiritish. Reducer'da case "tasdiqlandi": if (event.type === "YANGI") return { status: "kiritish" };. Diagrammaga bitta strelka, jadvalga bitta katak — o'zgarish faqat shu joyda. Qolgan holatlar YANGI ni e'tiborsiz qoldiradi.

5. Kutubxonalar: XState va Zag

Qo'lda yozilgan reducer-mashina kichik oqimlar uchun yetarli. Oqim murakkablashsa — ichma-ich holatlar ("to'lov" ichida "karta" va "Click"), parallel holatlar, taymerlar ("30 soniyada javob bo'lmasa — xato"), shartli o'tishlar — maxsus kutubxonalar bor. Ular bilan Holat mashinalari: XState darsida ishlaymiz. Hozir — tanishuv.

XState (xstate 5.33.2, React uchun @xstate/react 6.1.0; npm, 2026-10-13) — holat mashinalari va "statechart"lar kutubxonasi. Yuqoridagi bron mashinasi XState'da:

js
import { createActor, createMachine } from "xstate";

const bookingMachine = createMachine({
  id: "bron",
  initial: "kiritish",
  states: {
    kiritish: { on: { YUBOR: "yuborilmoqda" } },
    yuborilmoqda: {
      on: { TASDIQ: "tasdiqlandi", XATO: "xato" },
    },
    xato: { on: { YUBOR: "yuborilmoqda" } },
    tasdiqlandi: { type: "final" },
  },
});

const actor = createActor(bookingMachine);
actor.subscribe((snapshot) => console.log(snapshot.value));
actor.start();
actor.send({ type: "TASDIQ" }); // kiritishda yo'q — e'tiborsiz
actor.send({ type: "YUBOR" });
actor.send({ type: "XATO" });
actor.send({ type: "YUBOR" });
actor.send({ type: "TASDIQ" });
console.log("status:", actor.getSnapshot().status);

Toza papkada xstate@5.33.2 bilan Node 24 da ishga tushirdik:

text
kiritish
kiritish
yuborilmoqda
xato
yuborilmoqda
tasdiqlandi
status: done

Mashina — oddiy obyekt: bizning jadval deyarli so'zma-so'z. Ikkinchi kiritish — rad etilgan TASDIQ: kuzatuvchi xabar oldi, holat esa o'sha. type: "final" — yakuniy holat, shuning uchun oxirida status: done. React'da @xstate/react dagi useMachine(bookingMachine) [snapshot, send] qaytaradi — useReducer ga juda o'xshash.

Zag (@zag-js/react 1.46.0) — boshqa yo'nalish: UI komponentlari (menyu, dialog, slayder, combobox) uchun tayyor holat mashinalari. Ark UI va Chakra UI kabi kutubxonalar ichida ishlaydi. Siz "ochiq/yopiq, fokus qayerda, klaviatura nima qiladi" mantig'ini yozmaysiz — mashina tayyor, siz faqat ko'rinishni chizasiz.

Vosita Qachon
useState bitta-ikkita mustaqil qiymat
useReducer + status bir nechta hodisa, 3–6 holat (bron, to'lov, yuklash)
useImmerReducer chuqur ichma-ich holat
XState ko'p bosqichli, ichma-ich/parallel holatlar, taymerlar
Zag (va undan qurilgan UI kutubxonalari) tayyor murakkab UI vidjetlari

6. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
ekranda "Yuborilmoqda" va "Xato" birga mustaqil boolean'lar bitta status
tugmani ikki marta bosish ikki so'rov yubordi holat hodisani tekshirmaydi "yuborilmoqda" da YUBOR ni e'tiborsiz qoldiring
[Immer] An immer producer returned a new value *and* modified its draft… qavssiz strelka yoki return + o'zgartirish yoki o'zgartiring, yoki qaytaring
Cannot perform 'get' on a proxy that has been revoked qoralama retseptdan tashqarida ishlatildi qoralama faqat retsept ichida
Cannot add property …, object is not extensible Immer natijasini o'zgartirish yangi produce yoki dispatch
TS2339: Property 'bookingId' does not exist… holatni tekshirmasdan ma'lumot o'qildi avval status ni tekshiring

7. Mashqlar

Mashqlar kurs/mashqlar/17/40-holat-mashinasi/ da. 3-mashq uchun papkada npm i immer@11.1.21.

1-mashq (oson): imkonsiz holatlarni sanang

Taom rasmini yuklash komponentida to'rtta boolean bor: isIdle, isLoading, isLoaded, isFailed. Ma'noli holatlar — har safar faqat bittasi true.

  • Jami kombinatsiyalar soni:
  • Ma'noli holatlar soni:
  • Imkonsiz holatlar soni:
Yechim
js
const flags = 4;
const all = 2 ** flags;
const valid = flags; // har safar faqat bittasi true
console.log(all, valid, all - valid);

Konsolda:

text
16 4 12

To'rtta bayroq o'rniga status: "kutish" | "yuklanmoqda" | "yuklandi" | "xato" — 4 qiymat, 0 imkonsiz holat. isIdle umuman kerak emas edi: u "boshqa hech biri emas" degani.

2-mashq (o'rta): rasm yuklash mashinasi

TypeScript'da imageReducer(state, event) yozing. Holatlar: kutish, yuklanmoqda, yuklandi (url bilan), xato (message bilan, attempts — nechanchi urinish). Hodisalar: YUKLA, TAYYOR (url), XATO (message). Qoidalar:

  • kutish va xato dan YUKLA → yuklanmoqda;
  • yuklanmoqda dan TAYYOR → yuklandi, XATO → xato (attempts — oldingi xatodagi son + 1, birinchi xatoda 1);
  • xato da attempts 2 ga yetgan bo'lsa, YUKLA e'tiborsiz (boshqa urinish yo'q);
  • qolgan hamma kombinatsiya — o'sha holat.

Ishora: attempts ni yuklanmoqda holatida ham saqlash kerak bo'ladi (nechanchi urinish ketayotgani).

Yechim
tsx
import { useReducer } from "react";

export type ImageState =
  | { status: "kutish" }
  | { status: "yuklanmoqda"; attempts: number }
  | { status: "yuklandi"; url: string }
  | { status: "xato"; message: string; attempts: number };

export type ImageEvent =
  | { type: "YUKLA" }
  | { type: "TAYYOR"; url: string }
  | { type: "XATO"; message: string };

export const MAX_ATTEMPTS = 2;

export function imageReducer(
  state: ImageState,
  event: ImageEvent,
): ImageState {
  switch (state.status) {
    case "kutish":
      return event.type === "YUKLA"
        ? { status: "yuklanmoqda", attempts: 0 }
        : state;
    case "xato":
      if (event.type === "YUKLA" && state.attempts < MAX_ATTEMPTS) {
        return { status: "yuklanmoqda", attempts: state.attempts };
      }
      return state;
    case "yuklanmoqda":
      if (event.type === "TAYYOR") {
        return { status: "yuklandi", url: event.url };
      }
      if (event.type === "XATO") {
        return {
          status: "xato",
          message: event.message,
          attempts: state.attempts + 1,
        };
      }
      return state;
    case "yuklandi":
      return state;
  }
}

export default function DishImage() {
  const [state, send] = useReducer(imageReducer, {
    status: "kutish",
  });
  const failed = state.status === "xato";
  return (
    <div>
      <p>
        {state.status}
        {failed && ` (${state.attempts}): ${state.message}`}
      </p>
      <button onClick={() => send({ type: "YUKLA" })}>
        Yukla
      </button>{" "}
      <button onClick={() => send({ type: "XATO", message: "404" })}>
        Xato
      </button>
    </div>
  );
}

Ekranda:

text
xato (2): 404
Yukla Xato

Ikki marta xato bo'lgach, uchinchi «Yukla» e'tiborsiz qoldi. E'tibor bering: switch (state.status) da default yo'q. Lekin TypeScript hamma holatlar qamrab olinganini biladi — funksiya har yo'lda ImageState qaytaradi. attempts faqat kerakli holatlarda bor — kutish va yuklandi da bu maydon umuman yo'q.

3-mashq (qiyin): ikki filial, Immer bilan

«Bahor» kassasi holati: { branches: [{ id, orders: [{ id, dish, qty, ready }] }] }. Immer'ning produce i bilan reducer yozing. Amallar:

  • { type: "qtyChanged", branchId, orderId, qty } — sonni o'zgartiradi; qty 0 bo'lsa — buyurtma o'chadi;
  • { type: "allReady", branchId } — filialdagi hamma buyurtma ready: true;
  • { type: "branchClosed", branchId } — filial ro'yxatdan chiqadi.

Mavjud bo'lmagan filial yoki buyurtma — o'sha holat qaytsin (Immer'da buning uchun qoralamani o'zgartirmaslik yetadi). Tekshiring: tegilmagan filial yangi holatda o'sha obyekt bo'lsin.

Yechim
js
import { produce } from "immer";

export const cashierReducer = produce((draft, action) => {
  const branch = draft.branches.find((b) => b.id === action.branchId);
  if (!branch) return; // o'zgarish yo'q — asl holat qaytadi
  switch (action.type) {
    case "qtyChanged": {
      const { orders } = branch;
      const i = orders.findIndex((o) => o.id === action.orderId);
      if (i === -1) return;
      if (action.qty <= 0) orders.splice(i, 1);
      else orders[i].qty = action.qty;
      break;
    }
    case "allReady":
      for (const o of branch.orders) o.ready = true;
      break;
    case "branchClosed":
      draft.branches = draft.branches.filter((b) => b !== branch);
      break;
    default:
      throw new TypeError(`Noma'lum amal: ${action.type}`);
  }
});

const order = (id, dish, qty) => ({ id, dish, qty, ready: false });
const state = {
  branches: [
    { id: "chilonzor", orders: [order(1, "Osh", 1)] },
    { id: "yunusobod", orders: [order(2, "Manti", 3)] },
  ],
};
const send = (s, action) => cashierReducer(s, action);

const a = send(state, { type: "allReady", branchId: "chilonzor" });
console.log(a.branches[0].orders[0].ready);
console.log(a.branches[1] === state.branches[1]);
const b = send(a, {
  type: "qtyChanged", branchId: "x", orderId: 1, qty: 2,
});
console.log(b === a);
const c = send(a, { type: "branchClosed", branchId: "yunusobod" });
console.log(c.branches.map((x) => x.id));

npm i immer@11.1.21 bilan Node 24 da:

text
true
true
true
[ 'chilonzor' ]

Filial topilmasa, retsept hech narsa qaytarmaydi (return;). Immer'da bu "o'zgarish yo'q" degani: asl holat qaytadi va React qayta chizmaydi. Filial yopilganda draft.branches ga yangi massiv tayinlandi — bu ham qoralamani o'zgartirish, qaytarish emas. splice va o.ready = true — oddiy mutatsiya sintaksisi, lekin natija baribir yangi, muzlatilgan obyekt.

8. Real ishda

  • Redux Toolkit reducer'larini ichida Immer bilan yozadi — createSlice dagi state.value += 1 aynan qoralama (Redux Toolkit I). Immer'ni bilgan dasturchi RTK kodini o'qishda qiynalmaydi.
  • Server holati (yuklanmoqda/tayyor/xato) — TanStack Query kabi kutubxonalar ham status: "pending" | "error" | "success" beradi, boolean'lar o'rniga (TanStack Query I).
  • To'lov, ro'yxatdan o'tish, bron kabi ko'p bosqichli oqimlar holat mashinasi sifatida loyihalanadi; diagramma kod ko'rigida ham, mahsulot menejeri bilan gaplashishda ham qo'l keladi.
  • Intervyu savollari: "Imkonsiz holat nima, misol keltiring?", "Nega isLoading + isError o'rniga status?", "Immer qanday ishlaydi va nima uchun natijani muzlatadi?", "Holat mashinasini reducer bilan qanday yozasiz?".

Xulosa

  • Chuqur holatni spread bilan yangilash qiyin; produce va useImmerReducer qoralamani "o'zgartirish"ga imkon beradi, natija — yangi, muzlatilgan, tegilmagan qismlar — o'sha obyektlar.
  • Immer qoidasi: yo qoralamani o'zgartiring, yo yangi qiymat qaytaring; qoralama retseptdan tashqarida yashamaydi.
  • n ta boolean — 2ⁿ kombinatsiya, ko'pi imkonsiz; bitta status va discriminated union ularni turning o'zida yo'qotadi.
  • Holat mashinasi — holatlar, hodisalar va ruxsat etilgan o'tishlar jadvali; reducer'da noma'lum o'tish — o'sha holat.
  • Murakkab oqimlar uchun XState, tayyor UI mashinalari uchun Zag.

Keyingi dars: Context I: props drilling'siz ma'lumot uzatish — ma'lumotni daraxt bo'ylab oraliq komponentlarsiz yetkazamiz.

Manbalar

  • React: "Extracting State Logic into a Reducer" — Immer bilan reducer bo'limi — react.dev/learn/extracting-state-logic-into-a-reducer
  • React: "Choosing the State Structure" — imkonsiz holatlardan qochish — react.dev/learn/choosing-the-state-structure
  • Immer 11.1.21 — immerjs.github.io/immer; use-immer 0.11.0 — github.com/immerjs/use-immer
  • XState 5.33.2 — stately.ai/docs; Zag 1.46.0 — zagjs.com (npm versiyalari 2026-10-13 holati)
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
React reducer + Immer va holat mashinasi: ichma-ich holat va "imkonsiz holatlar"siz UI — IlmHamroh