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

React useReducer: murakkab state mantiqini bitta sof funksiyaga yig'ish

Qisqacha: useReducer(reducer, boshlangich) state'ni [holat, dispatch] juftligi bilan beradi. Komponent "nima bo'ldi"ni aytadi — dispatch({ type: "added", name: "Osh" }), holat qanday o'zgarishini esa bitta sof funksiya hal qiladi: reducer(holat, amal) → yangi holat. Mantiq hodisa ishlovchilari bo'ylab sochilib ketmaydi, React'siz sinaladi va har o'zgarishni amallar ro'yxati sifatida qayta o'ynatish mumkin. Reducer holatni o'zgartirmaydi, yon ta'sir qilmaydi va har doim holat qaytaradi.

Bu darsda

  • Reducer, amal (action) va dispatch nima ekanini Array.reduce orqali tushuntira olasiz.
  • Savat uchun reducer yozib, uni useReducer ga ulaysiz va "amal → holat" jadvalini tuzasiz.
  • Sof reducer qoidalarini va ular buzilganda nima bo'lishini bilasiz.
  • useState va useReducer orasida asosli tanlov qilasiz.
  • vazifalar dagi tarqoq setVazifalar/setTahrirId larni bitta reducer'ga yig'asiz.

Oldin bilishingiz kerak: State strukturasini loyihalash, State III: immutable yangilash, reduce: massivni bitta qiymatga yig'ish, Discriminated union.

1. Nega bu kerak?

«Bahor» saytida savat bor: taom qo'shish, olib tashlash, savatni tozalash. Sardor uni useState bilan yozdi. Keyin talablar keldi: "Savat tozalansa, promo-kod ham o'chsin", "Bir taomni ikki marta qo'shsa — soni 2 bo'lsin, qator ko'paymasin". Har talab boshqa ishlovchiga tushdi. Bir oydan keyin "savat qanday o'zgaradi?" degan savolga javob uchun beshta funksiyani o'qish kerak edi.

vazifalar da ham xuddi shu manzara. App.tsx da setVazifalar va setTahrirId o'nlab joyda chaqiriladi: almashtirishda, o'chirishda, serverdan ro'yxat kelganda, boshqa tabdan xabar kelganda. "Vazifa o'chirilsa, tahrir yopilsin" qoidasi oltita joyga yoyilgan — bittasini unutish oson.

Hayotiy o'xshatish: oshxona va buyurtma qog'ozi. Ofitsiant oshxonaga kirib qozonni o'zi aralashtirmaydi. U qog'ozga yozadi: "3-stol: osh, 2 ta". Oshpaz qog'ozni oladi va o'zining qoidasi bo'yicha bajaradi. Ofitsiant — dispatch, qog'oz — amal (action), oshpaz — reducer. Oshxona qoidalari bitta joyda — oshpazda.

2. Reducer nima

2.1 Array.reduce dan tanish

Nom tasodifiy emas. reduce darsida massivni bitta qiymatga yig'gan edik: [1, 2, 3].reduce((jami, son) => jami + son, 0). Akkumulyator har qadamda yangi qiymatga aylanadi. Endi massiv elementlari sonlar emas, hodisalar, akkumulyator esa — holat:

js
function cartReducer(cart, action) {
  switch (action.type) {
    case "added": {
      const found = cart.some((i) => i.name === action.name);
      if (!found) {
        const { name, price } = action;
        return [...cart, { name, price, qty: 1 }];
      }
      return cart.map((i) =>
        i.name === action.name ? { ...i, qty: i.qty + 1 } : i,
      );
    }
    case "removed":
      return cart.filter((i) => i.name !== action.name);
    case "cleared":
      return [];
    default:
      throw new TypeError(`Noma'lum amal: ${action.type}`);
  }
}

const actions = [
  { type: "added", name: "Osh", price: 35000 },
  { type: "added", name: "Ko'k choy", price: 5000 },
  { type: "added", name: "Osh", price: 35000 },
  { type: "removed", name: "Ko'k choy" },
];

const cart = actions.reduce(cartReducer, []);
for (const i of cart) console.log(i.name, i.qty, i.price * i.qty);

Konsolda:

text
Osh 2 70000

React yo'q, faqat JavaScript. To'rtta hodisa ketma-ket "qo'llandi" va oxirgi holat chiqdi. Bu reducer'ning eng kuchli tomoni: savat tarixini amallar ro'yxati sifatida saqlab, istalgan paytda qayta "o'ynatish" mumkin.

2.2 Uch atama

  • Holat (state) — ma'lumotning hozirgi ko'rinishi: savatdagi qatorlar.
  • Amal (action) — "nima bo'ldi" degan oddiy obyekt: { type: "added", name: "Osh", price: 35000 }. type — hodisa nomi, qolgani — tafsilot (inglizcha manbalarda payload, "yuk").
  • Reducer — (holat, amal) => yangi holat sof funksiyasi (Pure komponentlar dagi "sof" ma'nosida).

React'da bu uch narsa doira bo'ylab aylanadi:

flowchart LR
  U["Foydalanuvchi bosdi"] --> D["dispatch(amal)"]
  D --> R["reducer(holat, amal)"]
  R --> H["yangi holat"]
  H --> C["komponent qayta chiziladi"]
  C --> U

Komponent faqat ikki uchida ishtirok etadi: amal yuboradi va natijani chizadi. Holat qanday o'zgarishi — o'rtadagi bitta funksiyada.

Amal nomini o'tgan zamonda — bo'lgan voqea sifatida — yozish tavsiya etiladi: "added", "removed" ("qo'shildi"), "add" ("qo'sh") emas. Komponent buyruq bermaydi, faqat xabar beradi. Qanday javob berish — reducer'ning ishi. react.dev ham shunday maslahat beradi. vazifalar kanonida amallar o'zbekcha o'tgan zamonda: qoshildi, ochirildi, tahrirYopildi.

2.3 Amal → holat jadvali

Yuqoridagi to'rt amal savatni qanday o'zgartirdi:

Amal Holat oldin Holat keyin
added Osh [] [Osh×1]
added Ko'k choy [Osh×1] [Osh×1, Ko'k choy×1]
added Osh [Osh×1, Ko'k choy×1] [Osh×2, Ko'k choy×1]
removed Ko'k choy [Osh×2, Ko'k choy×1] [Osh×2]
cleared [Osh×2] []
paid (noma'lum) [Osh×2] TypeError

Bunday jadval reducer'ning "shartnomasi". Uni yozish oson, test qilish ham oson — har qator bitta assert («Vazifalar qadami» dagi testlarga qarang).

Tekshirib ko'ring: Reducer added da cart.push(...) qilib, cart ni qaytarsa, actions.reduce natijasi to'g'ri chiqadimi?

Javob

reduce natijasi to'g'ri chiqadi — u har qadamdagi qaytgan qiymatni oladi. Lekin boshlang'ich [] massivi ham o'zgarib ketadi, chunki hamma qadamda bitta massiv ishlatildi. React'da bu jiddiyroq: React yangi holatni eskisi bilan solishtiradi, o'sha massiv qaytsa — "o'zgarish yo'q" deb hisoblaydi. Pastda «Mutatsiya — ekran eskirib qoladi» bo'limida ko'ramiz.

3. useReducer

3.1 Sintaksis va birinchi savat

jsx
const [state, dispatch] = useReducer(reducer, initialArg, init?);
  • reducer — yuqoridagidek sof funksiya;
  • initialArg — boshlang'ich holat (yoki init uchun argument);
  • dispatch(action) — amal yuborish funksiyasi.
jsx
import { useReducer } from "react";

const MENU = [
  { name: "Osh", price: 35000 },
  { name: "Ko'k choy", price: 5000 },
];

function cartReducer(cart, action) {
  switch (action.type) {
    case "added": {
      const found = cart.some((i) => i.name === action.name);
      if (!found) {
        const { name, price } = action;
        return [...cart, { name, price, qty: 1 }];
      }
      return cart.map((i) =>
        i.name === action.name ? { ...i, qty: i.qty + 1 } : i,
      );
    }
    case "removed":
      return cart.filter((i) => i.name !== action.name);
    case "cleared":
      return [];
    default:
      throw new TypeError(`Noma'lum amal: ${action.type}`);
  }
}

function App() {
  const [cart, dispatch] = useReducer(cartReducer, []);
  const total = cart.reduce((sum, i) => sum + i.price * i.qty, 0);
  const remove = (name) => dispatch({ type: "removed", name });
  const clear = () => dispatch({ type: "cleared" });
  return (
    <section>
      <p>
        {MENU.map((m) => (
          <button key={m.name}
            onClick={() => dispatch({ type: "added", ...m })}>
            {m.name}
          </button>
        ))}
      </p>
      <ul>
        {cart.map((i) => (
          <li key={i.name}>
            {i.name} × {i.qty}{" "}
            <button onClick={() => remove(i.name)}>
              {i.name} ×
            </button>
          </li>
        ))}
      </ul>
      <p>Jami: {total} so'm</p>
      <button onClick={clear}>Tozalash</button>
    </section>
  );
}

Ekranda:

text
OshKo'k choy
Osh × 2 Osh ×
Jami: 70000 so'm
Tozalash

Hodisa ishlovchilarida hech qanday mantiq yo'q — faqat "nima bo'ldi". Savat qanday o'zgarishi bitta joyda: cartReducer da. total reducer'da emas, renderda hisoblanadi: u savatdan kelib chiqadi (hosilaviy qiymat).

3.2 Render jarayoni

Qadam Amal cart (state) Ekranda
boshida — [] Jami: 0 so'm
«Osh» added Osh [Osh×1] Osh × 1, Jami: 35000
«Ko'k choy» added Ko'k choy [Osh×1, Ko'k choy×1] Jami: 40000
«Osh» added Osh [Osh×2, Ko'k choy×1] Osh × 2, Jami: 75000
«Ko'k choy ×» removed Ko'k choy [Osh×2] Jami: 70000 so'm

3.3 dispatch — keyingi render uchun

dispatch holatni darhol o'zgartirmaydi. U amalni navbatga qo'yadi, reducer esa keyingi renderda ishlaydi — xuddi setState kabi (State II: snapshot). StrictMode'da ko'ramiz:

jsx
import { StrictMode, useReducer } from "react";
import { createRoot } from "react-dom/client";

function counterReducer(state, action) {
  console.log("reducer", action.type, state.n);
  return { n: state.n + 1 };
}

function App() {
  const [state, dispatch] = useReducer(counterReducer, { n: 0 });
  function handleClick() {
    dispatch({ type: "plus" });
    console.log("dispatch'dan keyin", state.n);
  }
  return <button onClick={handleClick}>+</button>;
}

createRoot(document.getElementById("root")).render(
  <StrictMode>
    <App />
  </StrictMode>,
);

Konsolda:

text
dispatch'dan keyin 0
reducer plus 0
reducer plus 0

Ekranda:

text
+

Uch narsa:

  1. dispatch dan keyin state.n hali 0 — bu renderning "surati".
  2. Reducer ishlovchi tugagandan keyin, keyingi renderda chaqirildi.
  3. StrictMode (faqat dev'da) reducer'ni ikki marta chaqirdi — u sof ekanini tekshirish uchun. Reducer ichida console.log dan boshqa yon ta'sir bo'lsa (so'rov, localStorage), u ham ikki marta bajarilardi. Reducer sof bo'lsa, ikki chaqiruv bir xil natija beradi va React bittasini tashlaydi.

Tekshirib ko'ring: Ishlovchida yangi holat kerak bo'lsa-chi (masalan, savat yangilangach serverga yuborish)? dispatch dan keyin state eski.

Javob

Reducer — oddiy sof funksiya, uni o'zingiz ham chaqira olasiz: const next = cartReducer(cart, action); dispatch(action); send(next);. React ham xuddi shu natijani oladi, chunki reducer bir xil kirishga bir xil javob beradi. vazifalar kanonida aynan shunday qilingan («Vazifalar qadami» da ko'rasiz).

4. Sof reducer qoidalari

4.1 Holatni o'zgartirmang

Sardor qisqa yo'l tanladi — massivga push qilib, o'shani qaytardi:

jsx
import { useReducer } from "react";

function cartReducer(state, action) {
  switch (action.type) {
    case "added":
      state.items.push(action.dish); // ❌ mutatsiya
      return state;
    default:
      return state;
  }
}

function Count({ items }) {
  console.log("Count", items.length);
  return <p>{items.length} ta taom</p>;
}

function App() {
  const [state, dispatch] = useReducer(cartReducer, {
    items: ["Osh"],
  });
  console.log("App", state.items.length);
  const add = () => dispatch({ type: "added", dish: "Manti" });
  return (
    <div>
      <Count items={state.items} />
      <button onClick={add}>Manti qo'shish</button>
    </div>
  );
}

Konsolda:

text
App 1
Count 1
App 2

Ekranda:

text
1 ta taom
Manti qo'shish

Qiziq natija. Bosilgandan keyin React App ni chaqirdi va u 2 ni ko'rdi (massiv haqiqatan o'zgargan). Lekin reducer o'sha obyektni qaytardi — Object.is(eski, yangi) → true. React "holat o'zgarmadi" deb qaror qildi. App ning yangi natijasini tashlab yubordi, Count ni chaqirmadi, ekranni yangilamadi. Ma'lumotda 2 ta taom, ekranda — 1 ta.

Qadam state.items (xotirada) Object.is(eski, yangi) Ekranda
boshida ["Osh"] — 1 ta taom
added (push) ["Osh", "Manti"] true — o'sha obyekt 1 ta taom

To'g'risi — yangi obyekt va yangi massiv: return { ...state, items: [...state.items, action.dish] } (State III).

Maslahat: Holat o'zgarmagan bo'lsa, o'sha holatni qaytarish — to'g'ri va foydali usul. React buni ko'rib, ekranni yangilamaydi. Faqat o'zgargan bo'lsa yangi obyekt yasang. Kanon reducer'imizdagi tahrirYopildi aynan shunday: tahrir allaqachon yopiq bo'lsa — o'sha holat.

4.2 return ni unutmang

switch da return unutilsa, ikki xil xato bo'ladi. Birinchisi — jim: yangi obyekt yasaldi-yu, qaytarilmadi. Ijro keyingi case ga tushib ketadi (switch):

jsx
import { useReducer } from "react";

function cartReducer(state, action) {
  switch (action.type) {
    case "added": {
      const items = [...state.items, action.dish]; // ❌ return yo'q
    }
    case "removed":
      return { ...state, items: state.items.slice(1) };
    default:
      return state;
  }
}

function App() {
  const [state, dispatch] = useReducer(cartReducer, {
    items: ["Osh"],
  });
  const add = () => dispatch({ type: "added", dish: "Manti" });
  return (
    <div>
      <p>{state.items.length} ta taom</p>
      <button onClick={add}>Qo'shish</button>
    </div>
  );
}

Ekranda:

text
0 ta taom
Qo'shish

«Qo'shish» bosildi, taom esa kamaydi: added dan removed ga tushib ketdi. Xato xabari yo'q — eng xavfli turdagi xato. Ikkinchisi — undefined qaytishi:

jsx
import { useReducer } from "react";

function cartReducer(state, action) {
  if (action.type === "paid") {
    // ❌ to'lov belgilanmadi, return ham yo'q — undefined
  } else {
    return state;
  }
}

function App() {
  const [state, dispatch] = useReducer(cartReducer, {
    items: ["Osh"],
  });
  return (
    <div>
      <p>{state.items.length} ta taom</p>
      <button onClick={() => dispatch({ type: "paid" })}>
        To'lash
      </button>
    </div>
  );
}

Konsolda:

text
Uncaught TypeError: Cannot read properties of undefined (reading 'items')

"undefined ning xususiyatlarini o'qib bo'lmaydi (items ni o'qishda)." Reducer undefined qaytardi, keyingi renderda state.items yiqildi. Xato reducer'da, lekin u chiqqan joy — komponent. Bunday xatoni "qayerda paydo bo'ldi" emas, "qiymat qayerdan keldi" deb qidiring (React ilovani debug qilish).

4.3 Yon ta'sirsiz va tasodifsiz

Reducer ichida bo'lmasligi kerak narsalar:

  • so'rov (fetch), localStorage ga yozish, console.log dan boshqa tashqi ish — ular effektda yoki ishlovchida;
  • Date.now(), Math.random(), crypto.randomUUID() — har chaqiruvda boshqa natija. StrictMode ikki marta chaqirsa, ikki xil natija bo'ladi. Bunday qiymatni ishlovchida yasab, amal ichida yuboring: dispatch({ type: "added", id: crypto.randomUUID(), … }).

Sof reducer'ning mukofoti — test. U React'siz, oddiy node:test bilan sinaladi (Birinchi avtomatik test): kirish — holat va amal, chiqish — yangi holat.

4.4 Noma'lum amal — xato

default da throw — amal nomida xato bo'lsa ("aded"), jim o'tib ketmasin. TypeScript bilan yana ham yaxshi: amallar discriminated union (Discriminated union) bo'lsa, xato nom kompilyatsiyada ushlanadi:

tsx
import { useReducer } from "react";

type CartAction =
  | { type: "added"; dish: string }
  | { type: "cleared" };

function cartReducer(items: string[], action: CartAction): string[] {
  return action.type === "added" ? [...items, action.dish] : [];
}

export default function App() {
  const [items, dispatch] = useReducer(cartReducer, []);
  const add = () => dispatch({ type: "add", dish: "Osh" });
  return <button onClick={add}>{items.length}</button>;
}

Xato:

text
TS2322: Type '"add"' is not assignable to type '"added" | "cleared"'.

Yangi amal turi qo'shilib, reducer'da unutilsa — never tekshiruvi ushlaydi:

tsx
type CartAction =
  | { type: "added"; dish: string }
  | { type: "removed"; dish: string }
  | { type: "cleared" };

type CartState = { items: readonly string[] };

export function cartReducer(
  state: CartState,
  action: CartAction,
): CartState {
  switch (action.type) {
    case "added":
      return { items: [...state.items, action.dish] };
    case "removed":
      return { items: state.items.filter((d) => d !== action.dish) };
    default: {
      const unhandled: never = action;
      const text = JSON.stringify(unhandled);
      throw new TypeError(`Noma'lum amal: ${text}`);
    }
  }
}

Xato:

text
TS2322: Type '{ type: "cleared"; }' is not assignable to type 'never'.

"cleared turi never ga mos emas" — ya'ni cleared uchun case yo'q. throw esa TypeScript ko'rmaydigan holat uchun qoladi: JavaScript fayldan yoki serverdan kelgan noto'g'ri amal.

5. Boshlang'ich holat: lazy init

Boshlang'ich holatni hisoblash qimmat bo'lsa (masalan, localStorage ni o'qib, tekshirish), uchinchi argument — init funksiyasi:

jsx
import { StrictMode, useReducer } from "react";
import { createRoot } from "react-dom/client";

function createInitialCart(dishes) {
  console.log("init", dishes.length);
  return { items: dishes };
}

function App() {
  const [cart] = useReducer(
    (state) => state,
    ["Osh", "Manti"],
    createInitialCart,
  );
  console.log("render");
  return <p>{cart.items.length} ta taom</p>;
}

createRoot(document.getElementById("root")).render(
  <StrictMode>
    <App />
  </StrictMode>,
);

Konsolda:

text
init 2
init 2
render
render

Ekranda:

text
2 ta taom

createInitialCart(["Osh", "Manti"]) faqat birinchi renderda chaqiriladi (StrictMode dev'da — ikki marta, tekshirish uchun). Keyingi renderlarda React uni chaqirmaydi. Agar useReducer(reducer, createInitialCart(dishes)) deb yozsangiz, funksiya har renderda ishlaydi va natijasi tashlab yuboriladi — useState(() => …) dagi bilan bir xil g'oya (useState).

6. useState mi, useReducer mi?

useReducer — useState ning "kuchliroq" varianti emas. Ikkalasi ham bir xil ishni qiladi, faqat mantiq qayerda yozilishi farq qiladi.

Mezon useState useReducer
kod hajmi kichik holatda kam oldindan ko'proq kod (amallar, switch)
o'qish oddiy yangilashda oson ko'p hodisali holatda oson — hammasi bir joyda
debug "qayerda o'zgardi?" — ko'p joyni qidirasiz har o'zgarish — bitta amal, reducer'ga log qo'yish yetadi
test komponent orqali reducer — React'siz, sof funksiya
bog'liq maydonlar ko'p setX — sinxron ushlab turish qiyin bitta amal bir nechta maydonni birga o'zgartiradi

Amaliy qoida: bitta-ikkita mustaqil qiymat (ochiq/yopiq, maydon matni) — useState. Bir nechta maydon birga o'zgarsa yoki bitta holatni ko'p joydan har xil yo'l bilan o'zgartirsangiz — useReducer. Bitta komponentda ikkalasi ham bo'lishi mumkin: kanonda vazifalar va tahrirId — reducer'da, qidiruv va tarmoq — useState da.

7. Vazifalar qadami: vazifalar va tahrir — reducer

7.1 Muammo raqamlarda

Holat 38 da App.tsx da setVazifalar va setTahrirId chaqiruvlari ko'p joyda edi. "Vazifa o'zgarsa tahrir yopilsin" qoidasi har ishlovchida qo'lda takrorlanardi. Server yoki boshqa tab ro'yxat yuborganda esa ikki updater funksiya ketma-ket chaqirilardi — biri ro'yxatni, biri tahrirni tekshirib. Yangi branch: refactor/reducer.

7.2 reducer.ts — bitta sof funksiya

Reducer o'zi hech narsani o'zgartirmaydi — ishni State III darsidagi amallar.ts bajaradi. U yerdagi qosh, almashtir, ochir, matnniOzgartir — v6 dagi VazifalarRoyxati klassi metodlarining sof nusxasi (o'sha mantiq, faqat yangi massiv qaytaradi). Ya'ni 13–16-qismda yozilgan va sinalgan mantiq qayta ishlatildi, reducer faqat "qaysi amalda qaysi funksiya" ni hal qiladi:

ts
// Discriminated union: tur — nima bo'lgani (o'tgan
// zamon); switch'dagi never tekshiruvi yangi amal
// unutilsa tsc xatosi beradi
export type VazifaAmali =
  | { readonly tur: "qoshildi"; readonly matn: string }
  | { readonly tur: "almashtirildi"; readonly id: number }
  | { readonly tur: "ochirildi"; readonly id: number }
  | {
      readonly tur: "matnOzgardi";
      readonly id: number;
      readonly matn: string;
    }
  // Butun ro'yxat keldi: server, boshqa tab, qaytarish, import
  | {
      readonly tur: "royxatKeldi";
      readonly vazifalar: readonly VazifaMalumoti[];
    }
  | { readonly tur: "tahrirBoshlandi"; readonly id: number }
  | { readonly tur: "tahrirYopildi" };

Kanonda type o'rniga o'zbekcha tur — ma'nosi bir xil, React maydon nomiga qaramaydi. Izoh qatorlari telefonga sig'ishi uchun qayta bo'lingan (bu va keyingi diff'larda), matni o'sha. Reducer'ning asosiy qismi:

ts
export function vazifalarReducer(
  holat: VazifalarHolati,
  amal: VazifaAmali,
): VazifalarHolati {
  switch (amal.tur) {
    case "qoshildi":
      return { ...holat, vazifalar: qosh(holat.vazifalar, amal.matn) };
    case "almashtirildi":
      return {
        vazifalar: almashtir(holat.vazifalar, amal.id),
        tahrirId: null,
      };
    case "ochirildi":
      return {
        vazifalar: ochir(holat.vazifalar, amal.id),
        tahrirId: null,
      };
    // … matnOzgardi, royxatKeldi, tahrirBoshlandi
    case "tahrirYopildi":
      return holat.tahrirId === null
        ? holat
        : { ...holat, tahrirId: null };
    default: {
      // Bu yerga faqat tsc ko'rmagan qiymat keladi (masalan, JS'dan)
      const qolmadi: never = amal;
      throw new TypeError(`Noma'lum amal: ${JSON.stringify(qolmadi)}`);
    }
  }
}

"Almashtirilsa yoki o'chirilsa — tahrir yopiladi" endi bitta joyda: tahrirId: null. royxatKeldi ning to'liq varianti (farq bo'lmasa — o'sha holat; tahrirdagi vazifa yo'qolgan bo'lsa — tahrir yopiladi) — kanon faylida. Amal → holat jadvali:

Amal vazifalar tahrirId
qoshildi oxiriga yangi o'zgarmaydi
almashtirildi, ochirildi, matnOzgardi o'zgaradi null
royxatKeldi (farq yo'q) o'sha holat — qayta chizish yo'q saqlanadi, agar vazifa bor bo'lsa
tahrirBoshlandi o'zgarmaydi amal.id
tahrirYopildi o'zgarmaydi null (allaqachon null bo'lsa — o'sha holat)

7.3 App.tsx

diff
-  const [vazifalar, setVazifalar] = useState(boshlangichRoyxat);
-  const [tahrirId, setTahrirId] = useState<number | null>(null);
+  const [holat, dispatch] = useReducer(
+    vazifalarReducer,
+    boshlangichRoyxat,
+    boshlangichHolat,
+  );
+  const { vazifalar, tahrirId } = holat;

Uchinchi argument boshlangichHolat — lazy init: localStorage dan kelgan ro'yxatdan { vazifalar, tahrirId: null } yasaydi. Ishlovchilar endi amal yuboradi:

diff
-    setTahrirId(id); // fokus va belgilash — TahrirQatori'niki
+    // Fokus va belgilash — TahrirQatori'ning effekti
+    dispatch({ tur: "tahrirBoshlandi", id });

Eng qiziq joy — optimistik yozuv. U avval ekranni yangilaydi, keyin serverga, boshqa tablarga yuboradi. Unga yangi ro'yxat darhol kerak, dispatch esa holatni keyingi renderda beradi. Reducer sof bo'lgani uchun uni shu yerda o'zimiz ham chaqiramiz:

diff
-    flushSync(() => setVazifalar(yangi));
+    // Reducer — sof funksiya: natijani shu yerda ham
+    // bilamiz (boshqa tablarga va serverga yuborish
+    // uchun), React ham xuddi shuni oladi
+    const yangi = vazifalarReducer(holat, amal).vazifalar;
+    flushSync(() => dispatch(amal));

Reducer'da Date.now() yoki so'rov bo'lganda bu yo'l ishlamasdi — ikki chaqiruv ikki xil natija berardi. server-sinxron.ts ham soddalashdi: ikki updater o'rniga bitta s.dispatch({ tur: "royxatKeldi", vazifalar: yangi }).

7.4 Testlar

reducer.test.js — 7 ta test, React'siz. Masalan, "bir xil ro'yxat kelsa — o'sha holat (qayta chizish yo'q)":

js
test("royxatKeldi: bir xil — O'SHA holat (qayta chizish yo'q)", () => {
  const nusxa = holat.vazifalar.map((v) => ({ ...v }));
  assert.equal(
    vazifalarReducer(holat, { tur: "royxatKeldi", vazifalar: nusxa }),
    holat,
  );
});

assert.equal — === bilan solishtiradi: aynan o'sha obyekt qaytgan bo'lishi kerak. Yana bir test JavaScript'dan kelgan noto'g'ri amalni tekshiradi:

js
assert.throws(() => vazifalarReducer(holat, { tur: "boshqa" }), {
  name: "TypeError",
  message: `Noma'lum amal: {"tur":"boshqa"}`,
});

Haqiqiy natija (npm run test):

text
ℹ tests 156
ℹ pass 156
ℹ fail 0
 Test Files  4 passed (4)
      Tests  24 passed (24)

node:test — 156 (+7), Vitest — 24 (o'zgarmadi: komponent xulqi bir xil). Lint, format, tsc -b toza; brauzer tekshiruvi — 159/159. Diff: 4 fayl, +229 −65.

Commit:

text
refactor: vazifalar va tahrir — useReducer (sof reducer)

8. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
ekran yangilanmaydi, ma'lumot o'zgargan reducer holatni push/= bilan o'zgartirib, o'shani qaytardi yangi obyekt va massiv
tugma boshqa ish qildi case da return yo'q — keyingi case ga tushdi har case da return
Cannot read properties of undefined (reading 'items') reducer undefined qaytardi har yo'lda holat qaytarilsin
dispatch dan keyin state eski holat keyingi renderda kerak bo'lsa reducer'ni o'zingiz chaqiring
dev'da so'rov ikki marta ketdi reducer'da yon ta'sir, StrictMode effekt yoki ishlovchiga
TS2322: … not assignable to type 'never' union'dagi amal reducer'da yo'q case qo'shing

9. Mashqlar

Mashqlar kurs/mashqlar/17/39-reducer/ da. Reducer'larni avval node --test bilan sinang, keyin komponentga ulang.

1-mashq (oson): amal → holat

Stol bron qilish reducer'i. Holat — { guests: 2, status: "bo'sh" }. Jadvalni to'ldiring:

js
function bookingReducer(state, action) {
  switch (action.type) {
    case "guestAdded":
      return { ...state, guests: Math.min(state.guests + 1, 12) };
    case "booked":
      return { ...state, status: "band" };
    case "cancelled":
      return { guests: 2, status: "bo'sh" };
    default:
      throw new TypeError(`Noma'lum amal: ${action.type}`);
  }
}
  • { guests: 2, status: "bo'sh" } + guestAdded → guests:
  • { guests: 12, status: "bo'sh" } + guestAdded → guests:
  • { guests: 5, status: "band" } + cancelled → status:
Yechim
js
function bookingReducer(state, action) {
  switch (action.type) {
    case "guestAdded":
      return { ...state, guests: Math.min(state.guests + 1, 12) };
    case "booked":
      return { ...state, status: "band" };
    case "cancelled":
      return { guests: 2, status: "bo'sh" };
    default:
      throw new TypeError(`Noma'lum amal: ${action.type}`);
  }
}

const start = { guests: 2, status: "bo'sh" };
console.log(bookingReducer(start, { type: "guestAdded" }).guests);
const full = { guests: 12, status: "bo'sh" };
console.log(bookingReducer(full, { type: "guestAdded" }).guests);
const booked = { guests: 5, status: "band" };
console.log(bookingReducer(booked, { type: "cancelled" }).status);

Konsolda:

text
3
12
bo'sh

Math.min mehmonlarni 12 dan oshirmaydi — chegara reducer'da, demak hamma tugma va hamma joy uchun bir xil.

2-mashq (o'rta): soni o'zgaradigan savat

Darsdagi cartReducer ga qtyChanged amalini qo'shing: { type: "qtyChanged", name, qty }. Qoidalar: qty 0 yoki undan kichik bo'lsa — qator o'chiriladi; 20 dan katta bo'lsa — 20; savatda yo'q taom uchun — o'sha holat (yangi massiv emas). Keyin uchta amalni reduce bilan qayta o'ynating.

Yechim
js
function cartReducer(cart, action) {
  switch (action.type) {
    case "added": {
      const found = cart.some((i) => i.name === action.name);
      if (!found) {
        const { name, price } = action;
        return [...cart, { name, price, qty: 1 }];
      }
      return cart.map((i) =>
        i.name === action.name ? { ...i, qty: i.qty + 1 } : i,
      );
    }
    case "qtyChanged": {
      if (!cart.some((i) => i.name === action.name)) return cart;
      if (action.qty <= 0) {
        return cart.filter((i) => i.name !== action.name);
      }
      const qty = Math.min(action.qty, 20);
      return cart.map((i) =>
        i.name === action.name ? { ...i, qty } : i,
      );
    }
    case "removed":
      return cart.filter((i) => i.name !== action.name);
    case "cleared":
      return [];
    default:
      throw new TypeError(`Noma'lum amal: ${action.type}`);
  }
}

const cart = [
  { type: "added", name: "Manti", price: 30000 },
  { type: "qtyChanged", name: "Manti", qty: 25 },
  { type: "qtyChanged", name: "Somsa", qty: 3 },
].reduce(cartReducer, []);
console.log(cart.map((i) => `${i.name}×${i.qty}`).join(", "));
const action = { type: "qtyChanged", name: "Somsa", qty: 1 };
const same = cartReducer(cart, action);
console.log(same === cart);

Konsolda:

text
Manti×20
true

Savatda yo'q taom uchun cart ning o'zi qaytadi — React buni "o'zgarish yo'q" deb tushunadi va qayta chizmaydi.

3-mashq (qiyin): bekor qilish va qaytarish

"Bekor qilish" (undo) va "qaytarish" (redo) har qanday reducer'ga qo'shiladigan o'rovchi yozing: undoable(reducer) yangi reducer qaytarsin. Uning holati — { past: [], present, future: [] } (Immutability chuqur dagi tarix g'oyasi):

  • { type: "undone" } — present future boshiga, past ning oxirgisi present ga; past bo'sh bo'lsa — o'sha holat;
  • { type: "redone" } — teskarisi;
  • boshqa har qanday amal — ichki reducer'ga; natija present dan farq qilsa, eski present past ga, future tozalanadi; farq qilmasa — o'sha holat.

Savat reducer'ini o'rab, «Osh», «Non», «Bekor», «Qayta» tugmalarini yasang.

Yechim
jsx
import { useReducer } from "react";

export function undoable(reducer) {
  return function (state, action) {
    const { past, present, future } = state;
    switch (action.type) {
      case "undone":
        if (past.length === 0) return state;
        return {
          past: past.slice(0, -1),
          present: past.at(-1),
          future: [present, ...future],
        };
      case "redone":
        if (future.length === 0) return state;
        return {
          past: [...past, present],
          present: future[0],
          future: future.slice(1),
        };
      default: {
        const next = reducer(present, action);
        if (next === present) return state;
        return {
          past: [...past, present],
          present: next,
          future: [],
        };
      }
    }
  };
}

function itemsReducer(items, action) {
  switch (action.type) {
    case "added":
      return [...items, action.name];
    default:
      throw new TypeError(`Noma'lum amal: ${action.type}`);
  }
}

const cartWithHistory = undoable(itemsReducer);

function App() {
  const [state, dispatch] = useReducer(cartWithHistory, {
    past: [],
    present: [],
    future: [],
  });
  const { past, present, future } = state;
  const add = (name) => dispatch({ type: "added", name });
  const undo = () => dispatch({ type: "undone" });
  const redo = () => dispatch({ type: "redone" });
  return (
    <div>
      <p>Savat: {present.join(", ") || "bo'sh"}</p>
      <button onClick={() => add("Osh")}>Osh</button>{" "}
      <button onClick={() => add("Non")}>Non</button>{" "}
      <button onClick={undo}>Bekor</button>{" "}
      <button onClick={redo}>Qayta</button>
      <p>Orqada: {past.length}, oldinda: {future.length}</p>
    </div>
  );
}

Ekranda:

text
Savat: bo'sh
Osh Non Bekor Qayta
Orqada: 0, oldinda: 2

Ikki amal ikki marta bekor qilindi — savat bo'sh, ikkala holat esa «oldinda» saqlanib turibdi. «Qayta» ni bossangiz, avval "Osh", keyin "Osh, Non" qaytadi. undoable ichki reducer haqida hech narsa bilmaydi: istalgan sof reducer'ni o'rash mumkin. Bu — reducer'ning sof bo'lishi bergan imkoniyat: holatlarni saqlab, oldinga-orqaga yurish.

10. Real ishda

  • Redux — butun ilova holati uchun reducer g'oyasi; Redux Toolkit'dagi createSlice aslida reducer va amallarni birga yasaydi (Redux Toolkit I). useReducer ni bilgan dasturchi Redux'ni tez o'rganadi.
  • Murakkab formalar va ko'p bosqichli jarayonlar (bron, buyurtma rasmiylashtirish) ko'pincha reducer bilan yoziladi: har qadam — amal, har holat — jadvaldagi qator. Keyingi darsda bu g'oyani holat mashinasigacha olib boramiz.
  • Kod ko'rigida "bu komponentda o'nta useState va ular birga o'zgaradi — reducer'ga yig'ing" degan izoh ko'p uchraydi.
  • Intervyu savollari: "useReducer qachon useState dan yaxshi?", "Reducer nega sof bo'lishi kerak?", "dispatch dan keyin holatni o'qisak nima bo'ladi?", "Redux va useReducer farqi nima?".

Xulosa

  • Reducer — (holat, amal) → yangi holat; Array.reduce dagi akkumulyator g'oyasining o'zi.
  • Amal — "nima bo'ldi" obyekti (type + ma'lumot), nomi o'tgan zamonda; dispatch uni yuboradi.
  • dispatch holatni keyingi renderda beradi; StrictMode reducer'ni ikki marta chaqirib, sofligini tekshiradi.
  • Mutatsiya — ekran eskiradi; return yo'q — jim xato yoki undefined; yon ta'sir va tasodif — reducer'dan tashqarida.
  • useReducer(reducer, arg, init) — lazy init; useState bilan farq — mantiq qayerda yozilishida.
  • vazifalar da 7 amalli sof reducer: tahrirni yopish qoidasi bir joyda, 7 ta node test, optimistik yozuv reducer'ni o'zi chaqiradi.

Keyingi dars: Reducer + Immer va holat mashinasi fikrlashi — ichma-ich holatni Immer bilan qisqa yozamiz va "imkonsiz holatlar"ni turlar bilan yo'qotamiz.

Manbalar

  • React: useReducer — react.dev/reference/react/useReducer
  • React: "Extracting State Logic into a Reducer" — react.dev/learn/extracting-state-logic-into-a-reducer
  • MDN: Array.prototype.reduce() — developer.mozilla.org
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
React useReducer: murakkab state mantiqini bitta sof funksiyaga yig'ish — IlmHamroh