Mundarija (31)
- Bu darsda
- 1. Nega bu kerak?
- 2. Birinchi store
- 2.1 create — store va hook birga
- 2.2 set — sayoz birlashtirish
- 2.3 get — amal ichida joriy holat
- 3. Selektor: faqat kerakli qism
- 3.1 Selektor qayta chizishni qanday cheklaydi
- 3.2 Tuzoq: selektor har safar yangi obyekt qaytaradi
- 3.3 Ikki yechim: alohida selektorlar va useShallow
- 4. Store React'dan tashqarida
- 4.1 getState, setState, subscribe
- 4.2 Marshrut action'i va test
- 4.3 Hook'siz store: createStore va useStore
- 5. Narxi va tajriba
- 6. Vazifalar qadami: e'lon — Zustand store'da (holat 22 boshlanishi)
- 6.1 Nima quramiz
- 6.2 Store: ilova-holati.ts (e'lon qismi)
- 6.3 Hook'ning tashqi ko'rinishi o'zgarmaydi: useElon.ts
- 6.4 Toast — Qobiq da, App faqat yozadi
- 6.5 Marshrut action'idan e'lon
- 6.6 Hozircha tekshiruv
- 7. Ko'p uchraydigan xatolar
- 8. Mashqlar
- 1-mashq (oson): selektorni to'ldiring
- 2-mashq (o'rta): cheksiz siklni tuzating
- 3-mashq (qiyin): bot va kuzatuvchi
- 4-mashq: Vazifalar qadami
- 9. Real ishda
- Xulosa
- Manbalar
Zustand React'da: create, set/get, selektor va useShallow bilan global store
Qisqacha: Zustand — React uchun kichik (gzip ~1 KB) global holat kutubxonasi.
create((set, get) => ({ ... }))store va uning hook'ini birga yaratadi: Provider kerak emas. Komponent store'dan selektor bilan faqat kerakli qismni oladi —useCartStore((s) => s.items.length)— va faqat shu qism o'zgarganda qayta chiziladi. Selektor har safar yangi obyekt qaytarsa —useShallow. Store React'dan tashqarida ham ishlaydi:getState(),setState(),subscribe().
Bu darsda
createbilan store yasaysiz, unga ma'lumot va amallarni birga joylaysiz.setqanday birlashtirishini (merge) vagetnima uchun kerakligini tushunasiz.- Selektor bilan qayta chizishni kamaytirasiz va buni konsolda sanab ko'rasiz.
- "getSnapshot should be cached" cheksiz siklini tanib,
useShallowyoki ikki selektor bilan tuzatasiz. - Store'ni komponentdan tashqarida — marshrut action'ida, taymerda, testda — ishlatasiz.
vazifalarda e'lonni (toast) Zustand store'ga ko'chirishni boshlaysiz.
Oldin bilishingiz kerak: State turlari xaritasi va vosita tanlash, useSyncExternalStore, Context II: reducer bilan, performance va chegaralar, React Router IV: data mode.
1. Nega bu kerak?
«Bahor» saytida mehmon menyudan taom tanlaydi. Tepada, sarlavhada — "Savatda: 3 ta". Pastda — menyu. Alohida sahifada — buyurtma ro'yxati. Uchala joy ham bitta savatni ko'radi.
Sardor birinchi bo'lib Context ni sinadi. Ishladi, lekin ikki muammo chiqdi:
- Ortiqcha chizish. Savat Context'ida ofitsiant ismi ham bor edi. Ofitsiant almashsa — savatni ko'rsatadigan hamma komponent qayta chizildi. Context II darsida buni ko'rgan edik: Context qiymati o'zgarsa, uni o'qigan har bir iste'molchi chiziladi, qaysi maydonni o'qiganidan qat'i nazar.
- React'dan tashqarida yetib bo'lmaydi. Telegram bot orqali kelgan buyurtma savatga tushishi kerak. Bot xabari — oddiy funksiya, komponent emas. Unda
useContextchaqirib bo'lmaydi (hooklar faqat komponent ichida).
Oldingi darsda xulosa qilgan edik: bir nechta sahifa ko'radigan, serverga tegishli bo'lmagan client state — global store ning ishi. Bugun eng kichik va eng ko'p ishlatiladigan store kutubxonasi bilan tanishamiz.
Hayotiy o'xshatish: oshxonadagi buyurtma doskasi. Doska bitta, devorda osig'liq. Oshpaz faqat "tayyorlanadi" ustunini ko'radi, kassir — "to'landi" ustunini. Kimdir yangi buyurtma yozsa, faqat o'sha ustunga qarab turgan odam bosh ko'taradi. Doskaga yozish uchun oshxonada turish shart emas — telefon orqali ham yozdirsa bo'ladi.
2. Birinchi store
2.1 create — store va hook birga
Zustand (nemischa "holat") — React uchun global holat kutubxonasi. Kursda versiyasi 5.0.15 (npm, 2026-08-13). Store (inglizcha "ombor") — komponentlardan tashqarida yashaydigan, o'zgarganda obunachilarga xabar beradigan bitta obyekt. useSyncExternalStore darsida qo'lda shunday "vanilla store" yozgan edik. Zustand — o'shaning tayyor, sinalgan varianti.
Saytdagi natija oynasida zustand bor — misollarni bosib ko'ring:
import { create } from "zustand";
const useCartStore = create((set) => ({
items: [],
add: (name) =>
set((state) => ({ items: [...state.items, name] })),
}));
function CartBadge() {
const count = useCartStore((state) => state.items.length);
return <p>Savatda: {count} ta</p>;
}
function Menu() {
const add = useCartStore((state) => state.add);
return (
<p>
<button onClick={() => add("Osh")}>Osh</button>{" "}
<button onClick={() => add("Manti")}>Manti</button>
</p>
);
}
export default function App() {
return (
<>
<CartBadge />
<Menu />
</>
);
}Ekranda (ikkala tugma bosilgach):
Savatda: 2 ta
Osh MantiUch narsaga qarang:
- Provider yo'q.
Apphech narsa bilan o'ralmagan. Store modul darajasida yaratildi va har komponent uni import qilib o'qiydi. createhook qaytaradi. Nomiusebilan boshlanadi — bu oddiy custom hook, hooklar qoidalari unga ham tegishli.- Ma'lumot va amal bir joyda.
items— holat,add— uni o'zgartiradigan funksiya. Ikkalasi ham store'da yashaydi.
create ga beriladigan funksiya — boshlang'ich holatni yasovchi. U bir marta, store yaratilganda chaqiriladi va set (hamda get, keyinroq) ni oladi. Qaytargan obyekti — store'ning birinchi holati.
2.2 set — sayoz birlashtirish
set ikki shaklda chaqiriladi:
set({ waiter: "Malika" })— yangi qiymat to'g'ridan-to'g'ri;set((state) => ({ items: [...state.items, name] }))— oldingi holatdan hisoblash (useState dagi updater funksiya kabi).
Muhim farq useState dan: set berilgan obyektni eski holat bilan birlashtiradi (merge). Siz faqat o'zgargan maydonni berasiz, qolganlari joyida qoladi:
import { create } from "zustand";
const useCartStore = create((set) => ({
items: ["Osh"],
waiter: "Sardor",
clear: () => set({ items: [] }),
}));
const before = useCartStore.getState();
before.clear();
const after = useCartStore.getState();
console.log("items:", after.items.length);
console.log("waiter:", after.waiter);
console.log("clear bormi:", typeof after.clear);
console.log("yangi obyekt:", before !== after);Konsolda:
items: 0
waiter: Sardor
clear bormi: function
yangi obyekt: trueset({ items: [] }) dan keyin waiter ham, clear ham joyida. Ichkarida Zustand taxminan Object.assign({}, eski, yangi) qiladi va yangi obyekt yaratadi. Eski holat o'zgarmaydi (before.items hamon ["Osh"]). Bu o'zgarmaslik qoidasining o'zi: React yangilanishni havola o'zgarganidan biladi.
Diqqat: birlashtirish faqat bir qavat. Ichma-ich obyektda
set({ settings: { theme: "qora" } })yozsangiz,settingsichidagi boshqa maydonlar yo'qoladi. Ichkarisini o'zingiz yoyasiz:set((s) => ({ settings: { ...s.settings, theme: "qora" } })). Chuqur ichma-ich holat uchunimmermiddleware — keyingi darsda.
set ning ikkinchi argumenti bor: set(yangi, true) — birlashtirmasdan almashtiradi. Shunda berilmagan maydonlar, funksiyalar ham, yo'qoladi. Kundalik ishda deyarli kerak bo'lmaydi. Asosan testda store'ni boshlang'ich holatga qaytarishda ishlatiladi («Store React'dan tashqarida» bo'limida).
2.3 get — amal ichida joriy holat
Ba'zan amal holatni o'qib, keyin qaror qilishi kerak. Masalan, "buyurtma berish": savat bo'sh bo'lsa — rad etish, bo'lmasa — buyurtma va savatni tozalash. Buning uchun boshlang'ich funksiyaning ikkinchi argumenti — get:
import { create } from "zustand";
const useCartStore = create((set, get) => ({
items: [],
orders: 0,
add: (name) =>
set((state) => ({ items: [...state.items, name] })),
checkout: () => {
const { items } = get();
if (items.length === 0) {
console.log("Savat bo'sh — buyurtma yo'q");
return;
}
console.log("Oshxonaga:", items.join(", "));
set((state) => ({ items: [], orders: state.orders + 1 }));
},
}));
export default function App() {
const orders = useCartStore((state) => state.orders);
const add = useCartStore((state) => state.add);
const checkout = useCartStore((state) => state.checkout);
return (
<p>
<button onClick={() => add("Osh")}>Osh</button>{" "}
<button onClick={checkout}>Buyurtma</button> — berildi:{" "}
{orders}
</p>
);
}Konsolda:
Savat bo'sh — buyurtma yo'q
Oshxonaga: OshEkranda:
Osh Buyurtma — berildi: 1get() — store'ning shu ondagi holati. Funksiya qachon chaqirilmasin, eskirgan qiymat bermaydi. Komponentdagi stale closure muammosi bu yerda yo'q: checkout render paytidagi items ni emas, bosilgan paytdagisini o'qiydi.
Qachon get, qachon set((state) => …)? Yangi qiymat faqat eskisidan hisoblansa — set ning updater shakli yetarli. Holatga qarab boshqa ish qilish kerak bo'lsa (log, rad etish, boshqa amalni chaqirish) — get.
Maslahat: amallarni store ichida saqlang —
add,checkout,clear. Komponent "nima bo'lsin" ni aytadi (checkout()), "qanday" — store'da. Bu useReducer dagi g'oyaning o'zi, faqatdispatch({ type })o'rniga to'g'ridan-to'g'ri funksiya.
Tekshirib ko'ring: Store:
{ items: ["Osh"], waiter: "Sardor", total: 35000 }.set({ items: [] })dan keyintotalnima bo'ladi?set({ items: [] }, true)dan keyin-chi?
Javob
Birinchisida total — 35000: set birlashtiradi, faqat items almashadi. Ikkinchisida (true — almashtirish) store butunlay { items: [] } bo'ladi: total ham, waiter ham undefined. Shuning uchun almashtirish rejimi kundalik amallarda xavfli — store'dagi funksiyalar ham yo'qoladi.
3. Selektor: faqat kerakli qism
3.1 Selektor qayta chizishni qanday cheklaydi
useCartStore((state) => state.items.length) dagi funksiya — selektor (selector): butun holatdan komponentga kerakli bo'lakni tanlab beruvchi funksiya. Buyurtma doskasidagi ustun kabi: oshpaz faqat o'z ustuniga qaraydi.
Zustand har set dan keyin har komponentning selektorini qayta chaqiradi va natijani oldingisi bilan Object.is orqali solishtiradi. Natija o'zgarmagan bo'lsa — komponent chizilmaydi. Sinab ko'ramiz. Ikki komponent: biri selektorsiz (butun store), biri selektor bilan. Faqat ofitsiant almashadi:
import { create } from "zustand";
const useCartStore = create((set) => ({
items: ["Osh"],
waiter: "Sardor",
setWaiter: (waiter) => set({ waiter }),
}));
function WholeStore() {
const state = useCartStore();
console.log("WholeStore chizildi");
return <p>Butun store: {state.items.length} ta</p>;
}
function OnlyCount() {
const count = useCartStore((state) => state.items.length);
console.log("OnlyCount chizildi");
return <p>Selektor: {count} ta</p>;
}
export default function App() {
const setWaiter = useCartStore((state) => state.setWaiter);
return (
<>
<WholeStore />
<OnlyCount />
<button onClick={() => setWaiter("Malika")}>
Ofitsiant: Malika
</button>
</>
);
}Konsolda:
WholeStore chizildi
OnlyCount chizildi
WholeStore chizildiBirinchi ikki qator — ilova ochilgandagi chizish. Uchinchisi — tugma bosilgandan keyin: faqat WholeStore qayta chizildi. U ekranda faqat sonni ko'rsatadi, lekin selektorsiz useCartStore() butun holatni qaytaradi. waiter o'zgardi — butun holat yangi obyekt — demak "o'zgardi". OnlyCount ning selektori esa ikkala safar ham 1 qaytardi: Object.is(1, 1) — chizish kerak emas.
| Yozuv | Qaytaradi | Qachon chiziladi |
|---|---|---|
useCartStore() |
butun holat | har qanday set dan keyin |
useCartStore((s) => s.items) |
massiv havolasi | items almashganda |
useCartStore((s) => s.items.length) |
son | soni o'zgarganda |
useCartStore((s) => s.add) |
funksiya | hech qachon (funksiya barqaror) |
Oxirgi qatorga e'tibor bering. Amallar store yaratilganda bir marta yasaladi va o'zgarmaydi. Faqat amal oladigan komponent (Menu) savat o'zgarganda ham chizilmaydi.
flowchart LR
B["Tugma: setWaiter"] --> S["set → yangi holat"]
S --> L1["WholeStore: (s) => s"]
S --> L2["OnlyCount: (s) => s.items.length"]
L1 --> R1["yangi obyekt → chiziladi"]
L2 --> R2["1 = 1 → chizilmaydi"]Diagrammada set dan keyingi yo'l: har obunachi o'z selektorini qayta ishlatadi va faqat natija o'zgarganlar chiziladi. Bu — useSyncExternalStore darsidagi "Zustand ichida" bo'limida ko'rgan kodning ishi: selektor getSnapshot ichida chaqiriladi.
3.2 Tuzoq: selektor har safar yangi obyekt qaytaradi
Sardor ikki qiymatni bitta selektor bilan olmoqchi bo'ldi — chiroyli ko'rinadi:
import { create } from "zustand";
const useCartStore = create((set) => ({
items: [],
add: (name) =>
set((state) => ({ items: [...state.items, name] })),
}));
function CartSummary() {
const { count, add } = useCartStore((state) => ({
count: state.items.length,
add: state.add,
}));
return <button onClick={() => add("Osh")}>Savatda: {count}</button>;
}
export default function App() {
return <CartSummary />;
}Konsolda:
The result of getSnapshot should be cached to avoid an infinite loop
Uncaught Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.Birinchi qator — React ogohlantirishi: "getSnapshot natijasi keshlanishi kerak, aks holda cheksiz sikl". Ikkinchisi — xato: "Yangilanish chuqurligi oshib ketdi. Komponent qayta-qayta setState chaqirganda shunday bo'ladi. React cheksiz siklning oldini olish uchun ichma-ich yangilanishlar sonini cheklaydi."
Nega aynan "getSnapshot"? Selektor useSyncExternalStore ning getSnapshot i ichida ishlaydi. ({ count, add }) — har chaqiruvda yangi obyekt. React snapshot'ni ikki marta olib solishtiradi: Object.is({…}, {…}) — har doim false. Demak "store o'zgardi" — qayta chizish — yana yangi obyekt — yana "o'zgardi". Sikl. Xato matnidagi componentWillUpdate — klass komponentlar davridan qolgan umumiy matn, bizning kodga aloqasi yo'q.
Maslahat: eski maqolalarda (Zustand 4) bunday selektor ishlardi — ortiqcha chizish bilan, lekin siklsiz. Zustand 5
useSyncExternalStorega to'liq tayanadi, shuning uchun endi xato. "Eski kodda ko'rsangiz" — quyidagi ikki yechimdan biri bilan almashtiring.
3.3 Ikki yechim: alohida selektorlar va useShallow
Birinchi yechim — har qiymatga alohida selektor. Har biri oddiy qiymat yoki barqaror havola qaytaradi:
import { create } from "zustand";
const useCartStore = create((set) => ({
items: [],
add: (name) =>
set((state) => ({ items: [...state.items, name] })),
}));
function CartSummary() {
const count = useCartStore((state) => state.items.length);
const add = useCartStore((state) => state.add);
return <button onClick={() => add("Osh")}>Savatda: {count}</button>;
}
export default function App() {
return <CartSummary />;
}Ekranda:
Savatda: 2Kanon kodi aynan shu uslubda: const elonQil = useIlovaHolati((h) => h.elonQil); — har qiymatga bir qator.
Ikkinchi yechim — useShallow. Sayoz solishtirish (shallow compare) — ikki obyektni havolasi bo'yicha emas, birinchi qavat maydonlari bo'yicha solishtirish: { count: 1, add: f } va yangi { count: 1, add: f } — teng. useShallow selektorni o'raydi: natija sayoz teng bo'lsa — oldingi obyektni qaytaradi. Havola o'zgarmaydi — sikl yo'q.
useShallow — zustand/react/shallow modulida. Avval «Savatda: 0» ni, keyin «Ofitsiant» ni bosing va konsolni kuzating:
import { create } from "zustand";
import { useShallow } from "zustand/react/shallow";
const useCartStore = create((set) => ({
items: [],
waiter: "Sardor",
add: (name) =>
set((state) => ({ items: [...state.items, name] })),
setWaiter: (waiter) => set({ waiter }),
}));
function CartSummary() {
const { count, add } = useCartStore(
useShallow((state) => ({
count: state.items.length,
add: state.add,
})),
);
console.log("CartSummary chizildi:", count);
return <button onClick={() => add("Osh")}>Savatda: {count}</button>;
}
function WaiterButton() {
const setWaiter = useCartStore((state) => state.setWaiter);
return (
<button onClick={() => setWaiter("Malika")}>Ofitsiant</button>
);
}
export default function App() {
return (
<>
<CartSummary />{" "}
<WaiterButton />
</>
);
}Konsolda:
CartSummary chizildi: 0
CartSummary chizildi: 1Ekranda:
Savatda: 1 OfitsiantSikl yo'q. Ofitsiant almashganda CartSummary chizilmadi: selektor yangi obyekt yasadi, lekin useShallow uni eskisi bilan sayoz solishtirdi (count — 1, add — o'sha funksiya) va eskisini qaytardi.
Massiv qaytaradigan selektor ham xuddi shu tuzoqqa tushadi: (s) => s.items.filter((i) => i.hot) — har safar yangi massiv. Davosi o'sha — useShallow (massiv elementlari sayoz solishtiriladi). Yoki hisoblashni komponentga oling: const items = useCartStore((s) => s.items); va pastda items.filter(...) — items havolasi faqat savat o'zgarganda almashadi.
Tekshirib ko'ring: Qaysi selektor cheksiz siklga olib keladi? (a)
(s) => s.items, (b)(s) => s.items.map((i) => i.toUpperCase()), (c)(s) => s.items.join(", ").
Javob
Faqat (b). (a) store'dagi massiv havolasini qaytaradi — set uni almashtirmaguncha o'sha havola. (c) satr qaytaradi — satrlar qiymat bo'yicha solishtiriladi: "Osh, Manti" === "Osh, Manti". (b) esa har chaqiruvda yangi massiv yasaydi. Davosi — useShallow yoki map ni komponentga olish.
4. Store React'dan tashqarida
4.1 getState, setState, subscribe
create qaytargan hook — funksiya. Unga Zustand bir nechta xususiyat (property) ham qo'shgan. Ular hook emas, istalgan joyda chaqiriladi:
| Xususiyat | Nima qiladi |
|---|---|
useCartStore.getState() |
joriy holat (amallar ham shu yerda) |
useCartStore.setState(x) |
set ning o'zi: birlashtiradi |
useCartStore.subscribe(fn) |
har o'zgarishda fn(yangi, eski); bekor qiluvchini qaytaradi |
useCartStore.getInitialState() |
boshlang'ich holat (testda qaytarish uchun) |
Endi «Nega bu kerak?» bo'limidagi ikkinchi muammo — Telegram buyurtmasi. Botdan xabar React'dan tashqarida keladi. Natija oynasida bot yo'q, uni setTimeout bilan taqlid qilamiz:
import { create } from "zustand";
const useCartStore = create((set) => ({
items: [],
add: (name) =>
set((state) => ({ items: [...state.items, name] })),
}));
// Kuzatuvchi: React'dan tashqarida, hook emas
const unsubscribe = useCartStore.subscribe((state, prev) => {
const was = prev.items.length;
console.log(`Savat: ${was} → ${state.items.length}`);
});
// "Telegram bot" — oddiy funksiya, komponent emas
function onTelegramOrder(name) {
useCartStore.getState().add(name);
}
setTimeout(() => {
onTelegramOrder("Lag'mon");
onTelegramOrder("Ko'k choy");
unsubscribe();
onTelegramOrder("Manti");
}, 200);
export default function App() {
const items = useCartStore((state) => state.items);
return <p>Savatda: {items.join(", ") || "bo'sh"}</p>;
}Konsolda:
Savat: 0 → 1
Savat: 1 → 2Ekranda:
Savatda: Lag'mon, Ko'k choy, MantiUchinchi buyurtma ham savatga tushdi va ekranda bor. Lekin konsolda uchinchi qator yo'q: unsubscribe() dan keyin kuzatuvchi xabar olmaydi. Komponent esa o'z obunasi bilan yangilandi — u subscribe ni useSyncExternalStore orqali o'zi boshqaradi.
subscribe ni nimaga ishlatishadi? Store o'zgarishini React'dan tashqaridagi narsaga yetkazish uchun: localStorage ga yozish, analitikaga hodisa, document.title ni yangilash. Komponent ichida esa doim hook — useCartStore(selektor). Komponentda getState() bilan o'qisangiz, komponent o'zgarishdan bexabar qoladi va eski qiymatni ko'rsatadi.
Diqqat:
getState()render ichida — xato. U obuna qilmaydi: store o'zgarsa, komponent qayta chizilmaydi. Qoida: render'da — hook, hodisa va tashqi kodda —getState().
4.2 Marshrut action'i va test
Bu imkoniyat vazifalar da aynan kerak bo'ladi. Data mode darsidagi marshrut action'i — oddiy async funksiya, komponent emas. U vazifani o'chiradi va / ga yo'naltiradi. "O'chirildi" e'loni esa ro'yxat sahifasida chiqishi kerak. Context'ga action'dan yetib bo'lmaydi. Store'ga — bo'ladi: useIlovaHolati.getState().elonQil(…). «Vazifalar qadami» bo'limida shuni qilamiz.
Ikkinchi joy — test. Store modul darajasida bitta: bir test o'zgartirgan holat keyingisiga o'tib qoladi. Har testdan keyin boshlang'ich holatga qaytaramiz:
import { create } from "zustand";
const useCartStore = create((set) => ({
items: [],
add: (name) =>
set((state) => ({ items: [...state.items, name] })),
}));
// 1-test
useCartStore.getState().add("Osh");
console.log("1-test:", useCartStore.getState().items);
// har testdan keyin — boshlang'ich holat (almashtirish rejimi)
useCartStore.setState(useCartStore.getInitialState(), true);
// 2-test toza store'dan boshlaydi
console.log("2-test:", useCartStore.getState().items);
console.log("add bormi:", typeof useCartStore.getState().add);Konsolda:
1-test: (1) ['Osh']
2-test: (0) []
add bormi: functiongetInitialState() — create dagi funksiya birinchi qaytargan obyekt, amallari bilan birga. Shuning uchun true (almashtirish) bu yerda xavfsiz: funksiyalar ham qaytadi. Kanonning IlovaHolati.test.tsx faylida xuddi shu maqsadda afterEach bor.
4.3 Hook'siz store: createStore va useStore
Ba'zan store'ga React umuman kerak emas — masalan, uni Node skriptida yoki boshqa freymvorkda ishlatasiz. Buning uchun createStore (vanilla, hook'siz) bor. React komponenti uni useStore(store, selektor) bilan o'qiydi:
import { createStore, useStore } from "zustand";
const tableStore = createStore((set) => ({
guests: 0,
seat: () => set((state) => ({ guests: state.guests + 1 })),
}));
console.log("boshida:", tableStore.getState().guests);
export default function App() {
const guests = useStore(tableStore, (state) => state.guests);
const seat = useStore(tableStore, (state) => state.seat);
return (
<p>
Mehmonlar: {guests}{" "}
<button onClick={seat}>+1 mehmon</button>
</p>
);
}Konsolda:
boshida: 0Ekranda:
Mehmonlar: 2 +1 mehmoncreate aslida shu ikkisining yig'indisi: createStore + uni o'qiydigan hook. Vanilla store bitta sahifada bir nechta nusxa kerak bo'lganda ham asqotadi (masalan, har ochiq oynaga alohida store). Bu naqsh keyingi darsda Next.js tuzog'i bilan birga to'liq ochiladi.
Tekshirib ko'ring:
function Badge() { const n = useCartStore.getState().items.length; return <p>{n}</p>; }— nima noto'g'ri?
Javob
getState() obuna qilmaydi. Badge birinchi chizishdagi sonni ko'rsatadi va savat o'zgarsa ham qayta chizilmaydi (boshqa sabab bilan chizilib qolguncha). Komponentda — useCartStore((s) => s.items.length).
5. Narxi va tajriba
Kutubxona bepul emas. vazifalar ga Zustand qo'shilganda birinchi yuklash +1.3 KB gzip oshdi (kanon o'lchovi). Bu kursdagi eng arzon qo'shimcha: router +29.4 KB, TanStack Query +9.9 KB edi.
Biz yana bir tajriba qildik (kanonga kirmaydi). Bir xil kichik vazifa — ro'yxatga qo'shish, belgilash, filtr va "qoldi" hisobi — yetti xil holat vositasi bilan. useReducer + Context varianti va Zustand varianti (tajriba/hajm.txt, React asosi bilan birga):
| Variant | gzip | Kod qatori |
|---|---|---|
useReducer + Context |
67 980 B | 39 |
| Zustand 5.0.15 | 68 367 B (+0.4 KB) | 14 |
Butun to'plamda farq bor-yo'g'i 0.4 KB: kutubxonaning bir qismini Context'ning Provider va reducer kodi "qoplaydi". Holat kodi esa 39 qatordan 14 qatorga tushdi. Zustand varianti — kanon tajribasidan aynan:
// holat-zustand.tsx — Zustand 5 (kanonda: ilova-holati.ts)
import { create } from "zustand";
import { Korinish, ishgaTushir } from "./_umumiy.tsx";
import type { Holat } from "./_umumiy.tsx";
const useHolat = create<Holat>()((set) => ({
vazifalar: [],
filtr: "hammasi",
qosh: (matn) => set((s) => ({ vazifalar: [...s.vazifalar, { id: s.vazifalar.length + 1, matn, bajarildi: false }] })),
almashtir: (id) => set((s) => ({ vazifalar: s.vazifalar.map((v) => (v.id === id ? { ...v, bajarildi: !v.bajarildi } : v)) })),
filtrla: (filtr) => set({ filtr }),
}));
function Ilova() {
return <Korinish holat={useHolat()} />;
}
ishgaTushir(<Ilova />);create<Holat>()(...) — ikki qavsli yozuv. TypeScript bilan Zustand shunday yoziladi; nega aynan shunday — keyingi darsda. Bu yerda useHolat() selektorsiz: Korinish hamma narsani ko'rsatadi, shuning uchun farqi yo'q. Qolgan besh variant (Redux Toolkit, Jotai, Valtio, MobX, XState) — keyingi darslarda, har biri o'z kuchli tomoni bilan.
6. Vazifalar qadami: e'lon — Zustand store'da (holat 22 boshlanishi)
6.1 Nima quramiz
vazifalar da e'lon (toast) — "«Non olish» qo'shildi.", "«…» o'chirildi." — hozir App ichidagi useElon hook'ida yashaydi (Custom hooklar I). Ikki muammo bor:
- Vazifa sahifasidan o'chirilsa — e'lon yo'q. Marshrut action'i (data mode) o'chiradi va
/ga yo'naltiradi. Lekin e'lon qilish uchunAppning hook'iga yetib bo'lmaydi. - Optimistik e'lon. Actions darsida form action ichidagi yangilanish action tugaguncha ko'rinmasdi. "Qo'shildi" e'loni darhol kerak edi — shuning uchun useOptimistic bilan aylanib o'tgan edik.
Store ikkalasini yechadi. Action getState() bilan yozadi. Store yozuvi esa useSyncExternalStore orqali transition ichida ham darhol chiziladi — optimistik qavat kerak emas.
Holat bu darsda boshlanadi, commit — keyingi darsda (sozlamalar va persist bilan birga, holat 22).
git switch -c feat/zustand
npm i --save-exact zustand@5.0.156.2 Store: ilova-holati.ts (e'lon qismi)
Kanon faylidan bugungi qism (izohlar telefonga sig'ishi uchun qisqa qatorlarga bo'lingan). Fayl boshidagi izoh nega Context emasligini o'lchov bilan aytadi:
// ilova-holati.ts — mijoz (client) holati: Zustand store
// (19/#21–#22). Server holati — Query'da (server-sinxron.ts);
// bu yerda — serverga tegishli bo'lmagan, lekin BIR NECHTA sahifa
// (va loader/action) ko'radigan narsalar: e'lon (toast) va
// foydalanuvchi sozlamalari.
// Nega Context emas (o'lchandi/sinaldi, QADAMLAR #20–#22):
// 1. Store React'dan TASHQARIDA ham bor: marshrut action'i
// (vazifa o'chirildi → ro'yxat sahifasida e'lon) getState()
// bilan yozadi — Context'ga hook'siz yetib bo'lmaydi
// 2. Selektor: komponent faqat o'zi o'qigan qism o'zgarsa
// chiziladi (e'lon o'zgarsa — faqat Bildirishnoma, App emas)
// 3. useSyncExternalStore ustida: action (transition) ichida
// yozilsa ham darhol chiziladi — 18/#05 dagi "optimistik
// e'lon" endi kerak emas
import { create } from "zustand";
type ElonQismi = {
/** ekran o'quvchi va toast uchun matn ("" — yo'q) */
readonly elon: string;
/** 3 s dan keyin o'chadi; yangisi taymerni boshidan boshlaydi */
elonQil: (matn: string) => void;
};
const ELON_MUDDATI = 3000;
let elonTaymeri: ReturnType<typeof setTimeout> | undefined;
const elonQismi: Qism<ElonQismi> = (set) => ({
elon: "",
elonQil(matn) {
clearTimeout(elonTaymeri);
set({ elon: matn });
if (matn !== "") {
elonTaymeri = setTimeout(() => set({ elon: "" }), ELON_MUDDATI);
}
},
});Tanish narsalar: set({ elon: matn }) — birlashtirish, boshqa maydonlarga tegmaydi. Taymer — store'dan tashqarida, modul o'zgaruvchisida (elonTaymeri). U ekranga chiqmaydi, shuning uchun holatda saqlash shart emas. 18-qismdagi useElon ham xuddi shu mantiqni useRef bilan qilgan edi.
Qism<ElonQismi> turi va useIlovaHolati ni yaratish (create + persist) — keyingi darsda: faylda e'londan tashqari sozlamalar qismi ham bor va ikkalasi slice naqshi bilan birlashadi. Bugun bilishingiz kerak bo'lgani — useIlovaHolati oddiy create hook'i: useIlovaHolati((h) => h.elon).
6.3 Hook'ning tashqi ko'rinishi o'zgarmaydi: useElon.ts
useElon ni ishlatgan joylar bor. Ularni buzmaslik uchun hook qoladi, faqat ichi store'ga o'tadi (kanon):
-import { useEffect, useRef, useState } from "react";
-
-const ELON_MUDDATI = 3000;
+import { useIlovaHolati } from "../ilova-holati.ts";
/** @returns [joriy e'lon, e'lon qilish funksiyasi] —
* useState kabi juftlik */
export function useElon(): [string, (matn: string) => void] {
- const [elon, setElon] = useState("");
- const taymer = useRef<ReturnType<typeof setTimeout>>(undefined);
- ...
+ // Selektor — faqat shu qism: sozlama o'zgarsa, bu hook chizmaydi
+ const elon = useIlovaHolati((h) => h.elon);
+ const elonQil = useIlovaHolati((h) => h.elonQil);
return [elon, elonQil];
}Ikki alohida selektor — «Ikki yechim» bo'limidagi birinchi usul. Bitta ({ elon, elonQil }) selektori bo'lsa — cheksiz sikl bo'lardi.
6.4 Toast — Qobiq da, App faqat yozadi
Bildirishnoma endi ildiz layout'da (layout marshrutlar) — hamma sahifada (kanon, Qobiq.tsx):
+import { Bildirishnoma } from "../komponentlar/Bildirishnoma.tsx";
+import { useElon } from "../hooklar/useElon.ts";
...
+ // 19/#21: e'lon — hamma sahifada (store'dan); ro'yxat
+ // sahifasidan tashqarida ham: masalan vazifa sahifasida
+ // o'chirilgach ro'yxatda
+ const [elon] = useElon();
...
<Outlet context={kontekst} />
+ <Bildirishnoma matn={elon} />App esa e'lonni faqat yozadi — matnni o'qimaydi (kanon, App.tsx):
- const [asosiyElon, elonQil] = useElon();
- const [elon, elonniKorsat] = useOptimistic(asosiyElon);
+ // E'lon — 19/#21 dan Zustand store'da: ekranda — Qobiq'dagi
+ // Bildirishnoma. App faqat yozadi (elonQil — barqaror
+ // funksiya; e'lon matni o'zgarsa App qayta chizilmaydi).
+ // 18/#05 dagi optimistik e'lon kerak emas: store yozuvi
+ // action ichida ham darhol chiziladi
+ const elonQil = useIlovaHolati((h) => h.elonQil);
...
- elonniKorsat(xabar); // darhol (optimistik); tasdiqlansa — asosiyga
+ // darhol; rad etilsa — "Saqlanmadi" bilan almashadi
+ elonQil(xabar);Selektor jadvalidagi oxirgi qator ishlayapti: App faqat funksiyani oladi. E'lon har 3 soniyada o'zgaradi, lekin App (butun ro'yxat!) chizilmaydi — faqat Qobiq dagi kichik Bildirishnoma.
6.5 Marshrut action'idan e'lon
Vazifa sahifasidagi «O'chirish» — vazifa-marshruti.ts dagi action. Endi u ham e'lon qiladi (kanon):
if (niyat === "ochir") {
+ const matn = sorovMijozi
+ .getQueryData<Vazifalar>(royxatKaliti)
+ ?.find((v) => v.id === raqam)?.matn;
await api.ochir(raqam, sozlama);
tasdiqlandi({ tur: "ochirildi", id: raqam });
+ // 19/#21: e'lon — store'da (React'dan tashqarida, hook'siz):
+ // ro'yxat sahifasida ko'rinadi
+ useIlovaHolati
+ .getState()
+ .elonQil(`«${matn ?? raqam}» o'chirildi.`);
return redirect("/");
}Vazifa matni Query keshidan olinadi (TanStack Query II), e'lon — getState() bilan. Action komponent emas, unda hook yo'q — «Store React'dan tashqarida» bo'limidagi Telegram bot holatining o'zi.
6.6 Hozircha tekshiruv
npm test hozir o'tadi, faqat bitta test o'zgaradi: e'lon endi App ichida emas. Kanonning App.test.tsx ida toast alohida kichik komponent bilan chiziladi va store'ga to'g'ridan-to'g'ri yoziladi:
+ // E'lon store'da — toast uni ko'rsatadi (App'ni qayta
+ // chizmasdan)
+ act(() => useIlovaHolati.getState().elonQil("Sinov e'loni"));
+ expect(elon?.textContent).toBe("Sinov e'loni");act — test kutubxonasidagi yordamchi: ichidagi yangilanish ekranga chiqib bo'lgach keyingi qatorga o'tadi (React Testing Library da to'liq). Brauzer tekshiruvlari, Sozlamalar sahifasi va commit — keyingi darsda.
7. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
The result of getSnapshot should be cached… va Maximum update depth exceeded |
selektor har safar yangi obyekt/massiv qaytaradi | alohida selektorlar yoki useShallow |
| boshqa maydon o'zgarsa ham komponent chiziladi | useStore() selektorsiz — butun holat |
useStore((s) => s.kerakli) |
| ichma-ich obyektning boshqa maydonlari yo'qoldi | set faqat bir qavat birlashtiradi |
{ ...s.settings, theme } yoki immer middleware |
| store o'zgaradi, ekran eski | render'da getState() — obuna yo'q |
render'da hook, getState() — hodisa va tashqi kodda |
amallar undefined bo'lib qoldi |
set(x, true) — almashtirish rejimi |
true siz; testda — getInitialState() bilan |
| testlar bir-biriga ta'sir qiladi | store modul darajasida bitta | afterEach da setState(getInitialState(), true) |
Invalid hook call |
useCartStore() oddiy funksiya yoki action ichida |
hook'siz joyda — useCartStore.getState() |
8. Mashqlar
Mashqlar kurs/mashqlar/19/21-zustand/ da. Har yechimni «Ishga tushir» bilan sinang.
1-mashq (oson): selektorni to'ldiring
Savatdagi taomlar narxi bilan saqlanadi. Total komponenti umumiy summani ko'rsatishi kerak. Selektor faqat son qaytarsin (shunda u faqat summa o'zgarganda chiziladi):
const total = useCartStore((state) =>
state.items.[___:reduce]((sum, item) => sum + item.price, 0),
);Ishora: massivni bitta qiymatga yig'ish — reduce.
Yechim
import { create } from "zustand";
const useCartStore = create((set) => ({
items: [],
add: (item) =>
set((state) => ({ items: [...state.items, item] })),
}));
function Total() {
const total = useCartStore((state) =>
state.items.reduce((sum, item) => sum + item.price, 0),
);
return <p>Jami: {total} so'm</p>;
}
export default function App() {
const add = useCartStore((state) => state.add);
return (
<>
<button onClick={() => add({ name: "Osh", price: 35000 })}>
Osh
</button>{" "}
<button onClick={() => add({ name: "Choy", price: 5000 })}>
Ko'k choy
</button>
<Total />
</>
);
}Ekranda:
Osh Ko'k choy
Jami: 40000 so'mreduce son qaytaradi — sonlar qiymat bo'yicha solishtiriladi, shuning uchun bu selektor xavfsiz: har safar hisoblaydi, lekin sikl yo'q.
2-mashq (o'rta): cheksiz siklni tuzating
Quyidagi kod «Ishga tushir» da xato beradi. Uni ikki usulda tuzatish mumkin: alohida selektorlar yoki useShallow. Birinchisini yozing.
import { create } from "zustand";
const useOrderStore = create((set) => ({
table: 4,
waiter: "Sardor",
next: () => set((state) => ({ table: state.table + 1 })),
}));
function OrderInfo() {
const { table, waiter } = useOrderStore((state) => ({
table: state.table,
waiter: state.waiter,
}));
return (
<p>
{table}-stol, ofitsiant: {waiter}
</p>
);
}
export default function App() {
return <OrderInfo />;
}Konsolda:
The result of getSnapshot should be cached to avoid an infinite loop
Uncaught Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.Yechim
import { create } from "zustand";
const useOrderStore = create((set) => ({
table: 4,
waiter: "Sardor",
next: () => set((state) => ({ table: state.table + 1 })),
}));
function OrderInfo() {
const table = useOrderStore((state) => state.table);
const waiter = useOrderStore((state) => state.waiter);
return (
<p>
{table}-stol, ofitsiant: {waiter}
</p>
);
}
export default function App() {
return <OrderInfo />;
}Ekranda:
4-stol, ofitsiant: SardorHar selektor oddiy qiymat qaytaradi: son va satr. Ikkinchi usul ham ishlaydi: useOrderStore(useShallow((s) => ({ table: s.table, waiter: s.waiter }))).
3-mashq (qiyin): bot va kuzatuvchi
«Bahor» savati uchun:
- Store:
items({ name, price }lar),add(item). - React'dan tashqarida
subscribe: umumiy summa birinchi marta 60 000 so'mdan oshganda konsolgaKatta buyurtma: <summa>yozsin va o'zini bekor qilsin (keyingi oshishlarda jim). - "Bot"
setTimeout(200 ms) ichida ketma-ket uchta taom qo'shadi: Osh 35 000, Manti 30 000, Lag'mon 28 000. - Komponent —
Jami: <summa> so'm.
Ishora: summa hisobini store'dan tashqariga, alohida funksiyaga chiqaring — uni kuzatuvchi ham, selektor ham ishlatadi.
Yechim
import { create } from "zustand";
const useCartStore = create((set) => ({
items: [],
add: (item) =>
set((state) => ({ items: [...state.items, item] })),
}));
const sumOf = (items) =>
items.reduce((sum, item) => sum + item.price, 0);
const unsubscribe = useCartStore.subscribe((state) => {
const total = sumOf(state.items);
if (total > 60000) {
console.log("Katta buyurtma:", total);
unsubscribe();
}
});
setTimeout(() => {
const { add } = useCartStore.getState();
add({ name: "Osh", price: 35000 });
add({ name: "Manti", price: 30000 });
add({ name: "Lag'mon", price: 28000 });
}, 200);
export default function App() {
const total = useCartStore((state) => sumOf(state.items));
return <p>Jami: {total} so'm</p>;
}Konsolda:
Katta buyurtma: 65000Ekranda:
Jami: 93000 so'mKuzatuvchi ikkinchi taomdan keyin (65 000) ishladi va o'zini o'chirdi. Uchinchi taom (93 000) — konsol jim, ekran esa yangilandi: komponent o'z obunasi bilan ishlaydi. unsubscribe kuzatuvchining ichida chaqirilyapti — bu mumkin, chunki const qiymati funksiya chaqirilganda o'qiladi, e'lon qilinganda emas.
4-mashq: Vazifalar qadami
feat/zustand branch'ida «Vazifalar qadami» bo'limidagi o'zgarishlarni qiling: zustand@5.0.15 o'rnatish, ilova-holati.ts (e'lon qismi), useElon.ts, Qobiq.tsx dagi Bildirishnoma, App.tsx dagi selektor, vazifa-marshruti.ts dagi getState(). Commit qilmang — keyingi darsda sozlamalar bilan birga.
Yechim
Tekshiruv ro'yxati:
npm run tip— toza.Qismturi hali yo'q bo'lsa, vaqtinchacreate<ElonQismi>()((set) => ({ ...elonQismi }))yozing yoki keyingi darsni kuting: kanon fayli ikkalasini birga beradi.npm run dev— vazifa qo'shing: e'lon darhol chiqadi (optimistik qatlamsiz). Vazifa sahifasida «O'chirish» — ro'yxatga qaytasiz va "«…» o'chirildi." ko'rinadi.- React DevTools Profiler (Profiler) bilan: e'lon chiqib-o'chganda
Appchizilmaydi, faqatQobiqvaBildirishnoma. git status—src/ilova-holati.tsyangi,package.jsonda"zustand": "5.0.15".
9. Real ishda
- Qayerda: Zustand — npm'da eng ko'p yuklanadigan React holat kutubxonalaridan biri. Kichik va o'rta loyihalarda Redux o'rnini egallagan: savat, foydalanuvchi sozlamalari, modal oynalar, tahrir rejimi.
- Qoida: server ma'lumoti — TanStack Query'da, URL'dagi narsa — router'da, forma — forma kutubxonasida. Store'ga faqat qolgani tushadi. Store'ga API javobini nusxalab qo'yish — Server state vs client state darsidagi xato.
- Bir store yoki ko'p? Zustand hujjati bitta store'ni (bo'laklarga bo'lingan holda) tavsiya qiladi. Bo'laklash — keyingi darsda.
- Intervyu savollari: "Zustand Context'dan nimasi bilan farq qiladi?", "selektor qayta chizishni qanday kamaytiradi?", "nega
(s) => ({ a: s.a })Zustand 5 da xato beradi?", "store'ni komponentdan tashqarida qanday o'qiysiz?".
Xulosa
create((set, get) => ({ ...holat, ...amallar }))— store va hook birga; Provider kerak emas.setbir qavat birlashtiradi va yangi obyekt yaratadi;get()— amal ichida joriy holat.- Selektor faqat kerakli qismni oladi; komponent faqat natija o'zgarganda chiziladi (
Object.is). - Selektor yangi obyekt yoki massiv qaytarsa — cheksiz sikl: alohida selektorlar yoki
useShallow. - React'dan tashqarida —
getState(),setState(),subscribe(); render'da esa doim hook. - Narxi:
vazifalarda +1.3 KB gzip; tajribada holat kodi 39 qatordan 14 ga tushdi.
Keyingi dars: Zustand II: middleware, slice'lar va SSR tuzoqlari — persist bilan sozlamalarni saqlash, devtools, immer, store'ni bo'laklarga bo'lish, TypeScript va Next.js'dagi tuzoq; vazifalar da Sozlamalar sahifasi va commit.
Manbalar
- Zustand: Introduction, Updating state, Selectors — zustand.docs.pmnd.rs (v5, 2026-10 holatiga)
- Zustand:
useShallow,createStore,useStore— zustand.docs.pmnd.rs/apis - Zustand v5 ga o'tish (selektor va
useSyncExternalStore) — zustand.docs.pmnd.rs/migrations/migrating-to-v5 - React:
useSyncExternalStore— react.dev/reference/react/useSyncExternalStore
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!