Mundarija (30)
- Bu darsda
- 1. Nega bu kerak?
- 2. Reducer nima
- 2.1 Array.reduce dan tanish
- 2.2 Uch atama
- 2.3 Amal → holat jadvali
- 3. useReducer
- 3.1 Sintaksis va birinchi savat
- 3.2 Render jarayoni
- 3.3 dispatch — keyingi render uchun
- 4. Sof reducer qoidalari
- 4.1 Holatni o'zgartirmang
- 4.2 return ni unutmang
- 4.3 Yon ta'sirsiz va tasodifsiz
- 4.4 Noma'lum amal — xato
- 5. Boshlang'ich holat: lazy init
- 6. useState mi, useReducer mi?
- 7. Vazifalar qadami: vazifalar va tahrir — reducer
- 7.1 Muammo raqamlarda
- 7.2 reducer.ts — bitta sof funksiya
- 7.3 App.tsx
- 7.4 Testlar
- 8. Ko'p uchraydigan xatolar
- 9. Mashqlar
- 1-mashq (oson): amal → holat
- 2-mashq (o'rta): soni o'zgaradigan savat
- 3-mashq (qiyin): bekor qilish va qaytarish
- 10. Real ishda
- Xulosa
- Manbalar
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
dispatchnima ekaniniArray.reduceorqali tushuntira olasiz. - Savat uchun reducer yozib, uni
useReducerga ulaysiz va "amal → holat" jadvalini tuzasiz. - Sof reducer qoidalarini va ular buzilganda nima bo'lishini bilasiz.
useStatevauseReducerorasida asosli tanlov qilasiz.vazifalardagi tarqoqsetVazifalar/setTahrirIdlarni 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:
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:
Osh 2 70000React 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 holatsof 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 --> UKomponent 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
addeddacart.push(...)qilib,cartni qaytarsa,actions.reducenatijasi 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
const [state, dispatch] = useReducer(reducer, initialArg, init?);reducer— yuqoridagidek sof funksiya;initialArg— boshlang'ich holat (yokiinituchun argument);dispatch(action)— amal yuborish funksiyasi.
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:
OshKo'k choy
Osh × 2 Osh ×
Jami: 70000 so'm
TozalashHodisa 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:
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:
dispatch'dan keyin 0
reducer plus 0
reducer plus 0Ekranda:
+Uch narsa:
dispatchdan keyinstate.nhali0— bu renderning "surati".- Reducer ishlovchi tugagandan keyin, keyingi renderda chaqirildi.
- StrictMode (faqat dev'da) reducer'ni ikki marta chaqirdi — u sof ekanini tekshirish uchun. Reducer ichida
console.logdan 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)?
dispatchdan keyinstateeski.
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:
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:
App 1
Count 1
App 2Ekranda:
1 ta taom
Manti qo'shishQiziq 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
tahrirYopildiaynan 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):
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:
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:
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:
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),localStoragega yozish,console.logdan 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:
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:
TS2322: Type '"add"' is not assignable to type '"added" | "cleared"'.Yangi amal turi qo'shilib, reducer'da unutilsa — never tekshiruvi ushlaydi:
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:
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:
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:
init 2
init 2
render
renderEkranda:
2 ta taomcreateInitialCart(["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:
// 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:
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
- 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:
- 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:
- 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)":
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:
assert.throws(() => vazifalarReducer(holat, { tur: "boshqa" }), {
name: "TypeError",
message: `Noma'lum amal: {"tur":"boshqa"}`,
});Haqiqiy natija (npm run test):
ℹ 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:
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:
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
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:
3
12
bo'shMath.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
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:
Manti×20
trueSavatda 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" }—presentfutureboshiga,pastning oxirgisipresentga;pastbo'sh bo'lsa — o'sha holat;{ type: "redone" }— teskarisi;- boshqa har qanday amal — ichki reducer'ga; natija
presentdan farq qilsa, eskipresentpastga,futuretozalanadi; farq qilmasa — o'sha holat.
Savat reducer'ini o'rab, «Osh», «Non», «Bekor», «Qayta» tugmalarini yasang.
Yechim
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:
Savat: bo'sh
Osh Non Bekor Qayta
Orqada: 0, oldinda: 2Ikki 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
createSliceaslida reducer va amallarni birga yasaydi (Redux Toolkit I).useReducerni 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
useStateva ular birga o'zgaradi — reducer'ga yig'ing" degan izoh ko'p uchraydi. - Intervyu savollari: "
useReducerqachonuseStatedan yaxshi?", "Reducer nega sof bo'lishi kerak?", "dispatchdan keyin holatni o'qisak nima bo'ladi?", "Redux vauseReducerfarqi nima?".
Xulosa
- Reducer —
(holat, amal) → yangi holat;Array.reducedagi akkumulyator g'oyasining o'zi. - Amal — "nima bo'ldi" obyekti (
type+ ma'lumot), nomi o'tgan zamonda;dispatchuni yuboradi. dispatchholatni keyingi renderda beradi; StrictMode reducer'ni ikki marta chaqirib, sofligini tekshiradi.- Mutatsiya — ekran eskiradi;
returnyo'q — jim xato yokiundefined; yon ta'sir va tasodif — reducer'dan tashqarida. useReducer(reducer, arg, init)— lazy init;useStatebilan farq — mantiq qayerda yozilishida.vazifalarda 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
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!