IlmHamroh
JavaScript Full-stack/19-qism. React: routing, server state, formalar21/36-dars27 daqiqa
Mundarija (31)

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

  • create bilan store yasaysiz, unga ma'lumot va amallarni birga joylaysiz.
  • set qanday birlashtirishini (merge) va get nima uchun kerakligini tushunasiz.
  • Selektor bilan qayta chizishni kamaytirasiz va buni konsolda sanab ko'rasiz.
  • "getSnapshot should be cached" cheksiz siklini tanib, useShallow yoki ikki selektor bilan tuzatasiz.
  • Store'ni komponentdan tashqarida — marshrut action'ida, taymerda, testda — ishlatasiz.
  • vazifalar da 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:

  1. 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.
  2. React'dan tashqarida yetib bo'lmaydi. Telegram bot orqali kelgan buyurtma savatga tushishi kerak. Bot xabari — oddiy funksiya, komponent emas. Unda useContext chaqirib 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:

jsx
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):

text
Savatda: 2 ta
Osh Manti

Uch narsaga qarang:

  1. Provider yo'q. App hech narsa bilan o'ralmagan. Store modul darajasida yaratildi va har komponent uni import qilib o'qiydi.
  2. create hook qaytaradi. Nomi use bilan boshlanadi — bu oddiy custom hook, hooklar qoidalari unga ham tegishli.
  3. 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:

jsx
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:

text
items: 0
waiter: Sardor
clear bormi: function
yangi obyekt: true

set({ 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, settings ichidagi boshqa maydonlar yo'qoladi. Ichkarisini o'zingiz yoyasiz: set((s) => ({ settings: { ...s.settings, theme: "qora" } })). Chuqur ichma-ich holat uchun immer middleware — 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:

jsx
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:

text
Savat bo'sh — buyurtma yo'q
Oshxonaga: Osh

Ekranda:

text
Osh Buyurtma — berildi: 1

get() — 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, faqat dispatch({ type }) o'rniga to'g'ridan-to'g'ri funksiya.

Tekshirib ko'ring: Store: { items: ["Osh"], waiter: "Sardor", total: 35000 }. set({ items: [] }) dan keyin total nima 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:

jsx
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:

text
WholeStore chizildi
OnlyCount chizildi
WholeStore chizildi

Birinchi 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:

jsx
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:

text
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 useSyncExternalStore ga 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:

jsx
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:

text
Savatda: 2

Kanon 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:

jsx
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:

text
CartSummary chizildi: 0
CartSummary chizildi: 1

Ekranda:

text
Savatda: 1 Ofitsiant

Sikl 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:

jsx
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:

text
Savat: 0 → 1
Savat: 1 → 2

Ekranda:

text
Savatda: Lag'mon, Ko'k choy, Manti

Uchinchi 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:

jsx
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:

text
1-test: (1) ['Osh']
2-test: (0) []
add bormi: function

getInitialState() — 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:

jsx
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:

text
boshida: 0

Ekranda:

text
Mehmonlar: 2 +1 mehmon

create 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:

tsx
// 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:

  1. Vazifa sahifasidan o'chirilsa — e'lon yo'q. Marshrut action'i (data mode) o'chiradi va / ga yo'naltiradi. Lekin e'lon qilish uchun App ning hook'iga yetib bo'lmaydi.
  2. 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).

bash
git switch -c feat/zustand
npm i --save-exact zustand@5.0.15

6.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:

ts
// 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):

diff
-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):

diff
+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):

diff
-  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):

diff
     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:

diff
+    // 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):

jsx
const total = useCartStore((state) =>
  state.items.[___:reduce]((sum, item) => sum + item.price, 0),
);

Ishora: massivni bitta qiymatga yig'ish — reduce.

Yechim
jsx
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:

text
Osh Ko'k choy
Jami: 40000 so'm

reduce 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.

jsx
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:

text
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
jsx
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:

text
4-stol, ofitsiant: Sardor

Har 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:

  1. Store: items ({ name, price } lar), add(item).
  2. React'dan tashqarida subscribe: umumiy summa birinchi marta 60 000 so'mdan oshganda konsolga Katta buyurtma: <summa> yozsin va o'zini bekor qilsin (keyingi oshishlarda jim).
  3. "Bot" setTimeout (200 ms) ichida ketma-ket uchta taom qo'shadi: Osh 35 000, Manti 30 000, Lag'mon 28 000.
  4. Komponent — Jami: <summa> so'm.

Ishora: summa hisobini store'dan tashqariga, alohida funksiyaga chiqaring — uni kuzatuvchi ham, selektor ham ishlatadi.

Yechim
jsx
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:

text
Katta buyurtma: 65000

Ekranda:

text
Jami: 93000 so'm

Kuzatuvchi 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. Qism turi hali yo'q bo'lsa, vaqtincha create<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 App chizilmaydi, faqat Qobiq va Bildirishnoma.
  • git status — src/ilova-holati.ts yangi, package.json da "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.
  • set bir 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: vazifalar da +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
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Zustand React'da: create, set/get, selektor va useShallow bilan global store — IlmHamroh