IlmHamroh
JavaScript Full-stack/19-qism. React: routing, server state, formalar22/36-dars28 daqiqa
Mundarija (34)

Zustand middleware: persist, devtools, immer, slice'lar va SSR tuzoqlari

Qisqacha: Middleware — create ga beriladigan funksiyani o'rab, store'ga yangi imkoniyat qo'shadigan qatlam. Masalan, persist holatni localStorage ga saqlaydi: partialize — nimani saqlash, version + migrate — eski yozuvni yangi shaklga ko'chirish. devtools — Redux DevTools'da har o'zgarishni ko'rsatadi, immer — state.a.b = 1 deb "o'zgartirib" yozishga imkon beradi. Katta store bo'laklarga (slice) bo'linadi. TypeScript'da — create<T>()(...). Serverda chiziladigan ilovada (Next.js) modul darajasidagi store hamma foydalanuvchiga umumiy bo'lib qoladi — har so'rovga alohida store kerak.

Bu darsda

  • persist bilan sozlamalarni saqlaysiz: name, storage, partialize, version, migrate.
  • devtools va immer middleware'larini ulaysiz va ularni qaysi tartibda o'rash kerakligini bilasiz.
  • Katta store'ni slice'larga bo'lasiz va TypeScript'da create<T>()(...) ni nega ikki qavs bilan yozilishini tushunasiz.
  • SSR'dagi ikki tuzoqni ajratasiz: umumiy store va hydration paytidagi birinchi kadr.
  • vazifalar da Sozlamalar sahifasini quriysiz va Zustand holatini commit qilasiz (holat 22).

Oldin bilishingiz kerak: Zustand I: kichik global store, localStorage va sessionStorage, Reducer + Immer va holat mashinasi fikrlashi, useSyncExternalStore.

1. Nega bu kerak?

Oldingi darsda «Bahor» savatini Zustand'ga ko'chirdik. Endi Jasur aka yangi talablar bilan keldi:

  1. "Mehmon «ixcham menyu» ni tanlasa, ertaga kirganda ham ixcham bo'lsin." Store xotirada yashaydi. Sahifa yangilansa — hammasi boshidan.
  2. Sardor savatga xato tushgan taomni qidiryapti: "Qaysi tugma, qachon, nimani o'zgartirdi?" console.log larni har joyga qo'yib chiqish — uzoq ish.
  3. Store kattalashdi: savat, mehmon ma'lumoti, sozlamalar, e'lonlar — 200 qatorli bitta fayl. Har kim o'sha faylga yozadi.

Uchala muammoning yechimi — Zustand'ning middleware lari va slice naqshi. To'rtinchi masala keyinroq chiqadi: sayt Next.js ga ko'chganda (23-qism). U yerda sahifa avval serverda chiziladi va bugungi kodning bir qismi xavfli bo'lib qoladi. Buni ham bugun ko'rib qo'yamiz.

Hayotiy o'xshatish: telefon g'ilofi. Telefon (sizning store funksiyangiz) o'zgarmaydi. Unga g'ilof kiydirasiz — u endi tushsa sinmaydi (persist). Ustidan yana bittasi — karta cho'ntagi bilan (devtools). Har qavat bitta imkoniyat qo'shadi va ichkaridagini o'zgartirmaydi.

2. persist — holatni saqlash

2.1 Eng oddiy ulash

Middleware — store yaratuvchi funksiyani o'rab, unga yangi xatti-harakat qo'shadigan funksiya. Zustand'da u oddiy funksiya chaqiruvi: create(persist(yaratuvchi, sozlama)). persist — zustand/middleware modulida, saytdagi natija oynasida bor.

Natija oynasida localStorage ning o'rinbosari (shim) ishlaydi: misol ichida saqlaydi va o'qiydi, lekin sahifa yangilanganda tozalanadi. Haqiqiy brauzerda yozuv saqlanib qoladi.

jsx
import { useEffect } from "react";
import { create } from "zustand";
import { persist } from "zustand/middleware";

const useSettingsStore = create(
  persist(
    (set) => ({
      compact: false,
      toggleCompact: () =>
        set((state) => ({ compact: !state.compact })),
    }),
    { name: "bahor:sozlamalar" },
  ),
);

export default function App() {
  const compact = useSettingsStore((state) => state.compact);
  const toggle = useSettingsStore((state) => state.toggleCompact);
  useEffect(() => {
    console.log(localStorage.getItem("bahor:sozlamalar"));
  });
  return (
    <button onClick={toggle}>
      Ixcham menyu{compact ? " ✓" : ""}
    </button>
  );
}

Konsolda:

text
null
{"state":{"compact":true},"version":0}

Ekranda:

text
Ixcham menyu ✓

Birinchi qator — ilova ochilganda: xotirada hali hech narsa yo'q. persist boshlang'ich holatni darhol yozmaydi, faqat birinchi set dan keyin. Ikkinchi qator — bosilgandan keyin. Yozuv shakli doim bir xil: { state, version }. state — saqlangan maydonlar, version — sxema raqami (sukut bo'yicha 0). Funksiyalar (toggleCompact) saqlanmaydi: JSON.stringify ularni tashlab yuboradi (localStorage darsidagi qoida).

Ilova qayta ochilganda persist store yaratilayotgan zahoti localStorage dan o'qiydi va boshlang'ich holatga birlashtiradi. localStorage sinxron, shuning uchun bu birinchi chizishdan oldin bo'ladi — birinchi kadrdayoq to'g'ri sozlama. Bu fakt «SSR tuzoqlari» bo'limida muhim bo'ladi.

2.2 storage — qayerga saqlash

Sukut bo'yicha — localStorage. Boshqasini storage sozlamasi bilan berasiz:

jsx
import { createJSONStorage, persist } from "zustand/middleware";

persist(yaratuvchi, {
  name: "bahor:savat",
  // tab yopilguncha saqlansin (masalan, to'lov oynasidagi savat)
  storage: createJSONStorage(() => sessionStorage),
});

createJSONStorage — getItem/setItem li har qanday saqlash joyi (storage) ustiga JSON qatlamini qo'yadi. Funksiya (() => sessionStorage) ichida berilishiga e'tibor bering: saqlash joyi faqat kerak bo'lganda olinadi. Serverda sessionStorage yo'q — to'g'ridan-to'g'ri yozilsa, modul yuklanishidayoq ReferenceError bo'lardi. Asinxron saqlash joylari (IndexedDB) ham ulanadi — IndexedDB kutubxonalari darsida ko'ramiz.

2.3 partialize — faqat keraklisi

Store'da e'lon matni ham bor. Uni saqlash kerak emas: ertaga kirgan mehmonga kechagi "Osh qo'shildi" e'loni ko'rinmasin. partialize — holatdan saqlanadigan qismni tanlaydigan funksiya:

jsx
import { useEffect } from "react";
import { create } from "zustand";
import { persist } from "zustand/middleware";

const useSettingsStore = create(
  persist(
    (set) => ({
      compact: false,
      toast: "",
      toggleCompact: () =>
        set((state) => ({
          compact: !state.compact,
          toast: "Sozlama saqlandi",
        })),
    }),
    {
      name: "bahor:sozlamalar",
      partialize: (state) => ({ compact: state.compact }),
    },
  ),
);

export default function App() {
  const toast = useSettingsStore((state) => state.toast);
  const toggle = useSettingsStore((state) => state.toggleCompact);
  useEffect(() => {
    console.log(localStorage.getItem("bahor:sozlamalar"));
  }, [toast]);
  return (
    <>
      <button onClick={toggle}>Ixcham menyu</button>
      <p>{toast}</p>
    </>
  );
}

Konsolda:

text
null
{"state":{"compact":true},"version":0}

Ekranda:

text
Ixcham menyu
Sozlama saqlandi

Ekranda e'lon bor, xotirada — yo'q. partialize selektorga o'xshaydi: holatni oladi, saqlanadigan obyektni qaytaradi. Kanonning ilova-holati.ts ida aynan shunday: sozlamalar saqlanadi, e'lon — yo'q.

Diqqat: localStorage ga hech qachon token, parol yoki shaxsiy ma'lumot qo'ymang. Sahifadagi istalgan skript (masalan, XSS orqali kirgan) uni o'qiy oladi. Bu saqlash joyi — sozlamalar va qoralamalar uchun.

2.4 version va migrate — eski yozuvni ko'chirish

Bir oydan keyin savat shakli o'zgardi. Avval taom nomlari saqlanardi: ["Osh"]. Endi miqdor ham kerak: [{ name: "Osh", qty: 1 }]. Lekin minglab mehmonning brauzerida eski shakl yotibdi. Yangi kod uni o'qisa — item.name — undefined.

Sxema versiyasi — saqlangan ma'lumot shaklining raqami. Shakl o'zgarsa, raqamni oshirasiz va migrate funksiyasini yozasiz: u eski yozuvni va uning versiyasini oladi, yangi shaklni qaytaradi. Quyida mehmonning brauzerida eski yozuv bor deb faraz qilamiz (birinchi qator uni qo'lda yozadi):

jsx
// mehmonning brauzerida — bir oy oldingi yozuv (versiya 0)
localStorage.setItem(
  "bahor:savat",
  JSON.stringify({ state: { items: ["Osh", "Manti"] }, version: 0 }),
);

import { create } from "zustand";
import { persist } from "zustand/middleware";

const useCartStore = create(
  persist(() => ({ items: [] }), {
    name: "bahor:savat",
    version: 1,
    migrate: (old, version) => {
      console.log("migrate, eski versiya:", version);
      if (version === 0) {
        return {
          items: old.items.map((name) => ({ name, qty: 1 })),
        };
      }
      return old;
    },
  }),
);

export default function App() {
  const items = useCartStore((state) => state.items);
  return (
    <ul>
      {items.map((item) => (
        <li key={item.name}>
          {item.name} × {item.qty}
        </li>
      ))}
    </ul>
  );
}

console.log(localStorage.getItem("bahor:savat"));

Konsolda:

text
migrate, eski versiya: 0
{"state":{"items":[{"name":"Osh","qty":1},{"name":"Manti","qty":1}]},"version":1}

Ekranda:

text
Osh × 1
Manti × 1

Ketma-ketlik: persist saqlangan version: 0 ni ko'rdi, kodda — 1. Farq bor — migrate chaqirildi. U qaytargan obyekt holatga tushdi va darhol yangi versiya bilan qayta yozildi (konsolning ikkinchi qatori). Keyingi ochilishda migrate chaqirilmaydi — versiyalar teng.

Versiyalar ko'paysa, migrate ichida zinapoya yoziladi: if (version < 1) {…}, keyin if (version < 2) {…}. Shunda 0-versiyali eski mehmon ham, 1-versiyali ham oxirgi shaklga yetadi. IndexedDB darsidagi upgradeneeded va oldVersion g'oyasining o'zi.

Diqqat: version ni oshirib, migrate ni yozmasangiz, Zustand konsolga xato yozadi: State loaded from storage couldn't be migrated since no migrate function was provided. Tarjimasi: "saqlangan holat ko'chirilmadi, chunki migrate funksiyasi berilmagan". Keyin saqlangan holatni tashlab yuboradi — mehmonning sozlamalari yo'qoladi. Shakl o'zgarsa — doim ikkalasi birga.

2.5 Boshqa tab — storage hodisasi

Mehmon saytni ikki tabda ochgan. Birinchisida «ixcham» ni yoqdi. Ikkinchisi buni bilmaydi: persist faqat store yaratilganda o'qiydi. localStorage darsidagi storage hodisasi — boshqa tab shu saytning localStorage ini o'zgartirganda keladi. Unga quloq solib, persist.rehydrate() — qayta o'qish — chaqiriladi. Kanondagi kod:

ts
// Boshqa tabda sozlama o'zgardi — persist buni o'zi bilmaydi:
// storage hodisasi (faqat BOSHQA tablarda keladi) bilan qayta
// o'qiymiz
addEventListener("storage", (event) => {
  if (event.key === SOZLAMALAR_KALITI) {
    void useIlovaHolati.persist.rehydrate();
  }
});

Natija oynasi bitta tab, shuning uchun bu yerda jonli ko'rsatib bo'lmaydi. Kanonning brauzer tekshiruvi ikki tab bilan sinadi («Vazifalar qadami» bo'limida natijasi). persist ning boshqa yordamchilari: persist.hasHydrated() — o'qib bo'ldimi, persist.clearStorage() — yozuvni o'chirish, onRehydrateStorage — o'qish tugaganda chaqiriladigan funksiya.

Tekshirib ko'ring: Store: { lang: "uz", draft: "", setLang }. partialize siz persist ulansa, localStorage da nima bo'ladi? Qaysi maydon saqlanmasligi kerak?

Javob

{"state":{"lang":"uz","draft":""},"version":0} — barcha ma'lumot maydonlari (funksiya setLang JSON'ga tushmaydi). draft — qoralama: uni saqlash ba'zan foydali (yozilgan matn yo'qolmasin), ba'zan zararli (birovning kompyuterida qolib ketadi). Qaror — sizniki, lekin uni partialize bilan ochiq qiling. Sukut bo'yicha "hammasi saqlanadi" — xavfli odat.

3. devtools va immer

3.1 devtools — har o'zgarish jurnalda

Redux DevTools — Chrome/Firefox kengaytmasi. U store'ning har o'zgarishini ro'yxat qilib ko'rsatadi: nomi, oldingi va keyingi holat, farqi. Hatto oldingi holatga "qaytib" ko'rish mumkin (time travel). Nomida Redux bo'lsa ham, Zustand unga devtools middleware orqali ulanadi:

jsx
import { create } from "zustand";
import { devtools } from "zustand/middleware";

const useCartStore = create(
  devtools(
    (set) => ({
      items: [],
      add: (name) =>
        set(
          (state) => ({ items: [...state.items, name] }),
          undefined,
          "savat/qoshish",
        ),
    }),
    { name: "Bahor savati", enabled: import.meta.env.DEV },
  ),
);

Uch yangi narsa:

  • set ning uchinchi argumenti — amal nomi. Jurnalda "anonymous" o'rniga savat/qoshish ko'rinadi. Ikkinchi argument (undefined) — o'tgan darsdagi almashtirish bayrog'i.
  • name — kengaytmada store nomi (bir nechta store bo'lsa, ajratish uchun).
  • enabled: import.meta.env.DEV — faqat ishlab chiqishda. import.meta.env.DEV — Vite beradigan bayroq (Vite: muhit o'zgaruvchilari). Production'da kengaytmasi bor har kim store ichini ko'ra olmasin.

Kengaytma bo'lmasa, devtools jim ishlaydi — xato bermaydi. Saytdagi natija oynasida kengaytma yo'q, shuning uchun bu misol tugmasiz.

3.2 immer — "o'zgartirib" yozish

Reducer + Immer darsida Immer bilan tanishgan edik: qoralama (draft) ni bemalol o'zgartirasiz, Immer esa undan yangi o'zgarmas obyekt yasaydi. Zustand'da ham chuqur ichma-ich holatda spread piramidasi o'sadi:

jsx
set((state) => ({
  branches: {
    ...state.branches,
    chilonzor: {
      ...state.branches.chilonzor,
      dishes: {
        ...state.branches.chilonzor.dishes,
        osh: { ...state.branches.chilonzor.dishes.osh, price },
      },
    },
  },
}));

immer middleware bilan xuddi shu bitta qator:

jsx
import { create } from "zustand";
import { immer } from "zustand/middleware/immer";

const useMenuStore = create(
  immer((set) => ({
    branches: {
      chilonzor: { dishes: { osh: { price: 35000, hot: true } } },
      yunusobod: { dishes: { osh: { price: 35000, hot: true } } },
    },
    setPrice: (branch, dish, price) =>
      set((state) => {
        state.branches[branch].dishes[dish].price = price;
      }),
  })),
);

const before = useMenuStore.getState();
before.setPrice("chilonzor", "osh", 38000);
const after = useMenuStore.getState();

console.log(after.branches.chilonzor.dishes.osh.price);
console.log(before.branches.chilonzor.dishes.osh.price);
console.log(before.branches.yunusobod === after.branches.yunusobod);
console.log(Object.isFrozen(after.branches));

zustand/middleware/immer alohida immer paketini talab qiladi (npm i immer), saytdagi natija oynasida u yo'q. Biz zustand 5.0.15 va immer 11.1.21 bilan Chrome 155 da ishga tushirdik:

text
38000
35000
true
true

Qatorma-qator: yangi narx — 38 000. Eski holat (before) o'zgarmagan — 35 000. Tegilmagan filial (yunusobod) — o'sha havola: Immer faqat o'zgargan yo'lni nusxalaydi (strukturaviy bo'lishish), shuning uchun Yunusobod filialini ko'rsatadigan komponent chizilmaydi. Oxirgisi: natija muzlatilgan (frozen) — tasodifan to'g'ridan-to'g'ri o'zgartirib bo'lmaydi.

Updater ichida ikki yo'ldan bittasini tanlang: qoralamani o'zgartiring yoki yangi obyekt qaytaring. Ikkalasi birga (state.x = 1; return { y: 2 }) — Immer xato beradi.

3.3 Middleware tartibi

Bir nechta middleware — ichma-ich o'raladi. Zustand hujjati tavsiya qiladigan tartib: devtools eng tashqarida, immer eng ichkarida:

jsx
const useStore = create(
  devtools(
    persist(
      immer((set) => ({ /* holat va amallar */ })),
      { name: "bahor" },
    ),
    { name: "Bahor" },
  ),
);

Nega shunday? set chaqirilganda u tashqaridan ichkariga o'tadi. Rasmga qarang:

flowchart LR
  A["amal: set(...)"] --> D["devtools: jurnalga yozadi"]
  D --> P["persist: localStorage'ga saqlaydi"]
  P --> I["immer: qoralamadan yangi obyekt"]
  I --> S["store: yangi holat"]

devtools tashqarida bo'lsa, boshqa middleware'larning ishini ham ko'radi (masalan, persist qayta o'qigan holatni). immer ichkarida bo'lsa, faqat sizning funksiyangizning set ini o'zgartiradi — boshqalar oddiy obyekt bilan ishlaydi. Tartib buzilsa ham ko'pincha ishlaydi, lekin TypeScript turlari va DevTools jurnali chalkashadi.

Tekshirib ko'ring: immer bilan: set((s) => { s.items.push("Osh"); }). Shu kod immer siz ishlaydimi?

Javob

Xatosiz bajariladi, lekin noto'g'ri ishlaydi. immer siz s — haqiqiy holat, push uni joyida o'zgartiradi va updater undefined qaytaradi. Zustand'ning set i "hech narsa o'zgarmadi" deb oladi, items havolasi o'sha — selektorlar o'zgarishni ko'rmaydi va ekran yangilanmaydi. Immer'siz — doim yangi massiv: set((s) => ({ items: [...s.items, "Osh"] })).

4. Slice naqshi va TypeScript

4.1 Store'ni bo'laklarga bo'lish

Slice (bo'lak) — store'ning bitta mavzuga tegishli qismi (holat va amallari) alohida funksiyada. Har bo'lak (set, get) oladi va o'z qismini qaytaradi. Store — bo'laklarning yig'indisi:

jsx
import { create } from "zustand";

const createCartSlice = (set, get) => ({
  items: [],
  add: (name) =>
    set((state) => ({ items: [...state.items, name] })),
  checkout: () => {
    const { items, guest } = get();
    console.log(`${guest}: ${items.join(", ")}`);
    set({ items: [] });
  },
});

const createGuestSlice = (set) => ({
  guest: "Malika",
  rename: (guest) => set({ guest }),
});

const useBahorStore = create((...a) => ({
  ...createCartSlice(...a),
  ...createGuestSlice(...a),
}));

export default function App() {
  const count = useBahorStore((state) => state.items.length);
  const add = useBahorStore((state) => state.add);
  const checkout = useBahorStore((state) => state.checkout);
  return (
    <p>
      <button onClick={() => add("Osh")}>Osh</button>{" "}
      <button onClick={checkout}>Buyurtma</button> ({count})
    </p>
  );
}

Konsolda:

text
Malika: Osh

Ekranda:

text
Osh Buyurtma (0)

(...a) — create bergan barcha argumentlar (set, get va store API) (rest va spread). Ular har bo'lakka aynan uzatiladi. Muhimi: bo'laklar bitta store'da. checkout savat bo'lagida yozilgan, lekin get() butun holatni beradi — mehmon ismini ham. Bo'lak — kodni fayllarga bo'lish usuli, ma'lumotni ajratish emas.

Nega bir nechta mustaqil store emas? Ikki store bo'lsa, checkout ikkinchisini import qilib o'qishi kerak, persist ikki marta sozlanadi, testda ikkalasini tozalash kerak. Zustand hujjati: "bitta store, kerak bo'lsa bo'laklarga bo'lingan". Butunlay bog'liq bo'lmagan narsalar uchun (masalan, alohida modulning ichki holati) alohida store ham normal.

4.2 TypeScript: create<T>()(...)

TypeScript bilan Zustand'ning odatiy yozuvi g'alati ko'rinadi — ikki qavs:

tsx
import { create } from "zustand";

type CartState = {
  items: string[];
  add: (name: string) => void;
};

const useCartStore = create<CartState>()((set) => ({
  items: [],
  add: (name) =>
    set((state) => ({ items: [...state.items, name] })),
}));

useCartStore.getState().add(42);

Xato:

text
TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.

Holat turini (CartState) siz berasiz. Middleware'lar esa store turini o'zgartiradi: persist hook'ga .persist xususiyatini qo'shadi, devtools — set ga uchinchi argumentni. Bu turlarni TypeScript o'zi chiqarishi kerak. Lekin TypeScript'da generik funksiyaning turlarini "qisman" berib bo'lmaydi: bittasini qo'lda bersangiz, qolganini ham berishingiz shart (Generiklar). create<CartState>() — birinchi chaqiruv faqat holat turini oladi. Ikkinchisi (...) — middleware'li funksiyani oladi va uning turlarini TypeScript o'zi chiqaradi. Ya'ni ikki qavs — tilning cheklovini aylanib o'tish (currying usuli bilan).

Bitta qavsli create<CartState>((set) => …) ham middleware'siz ishlaydi. Lekin persist qo'shilgan zahoti turlar buziladi. Shuning uchun hujjat va kanon doim ikki qavsni yozadi.

4.3 Slice'ning turi: StateCreator

Slice'larni TypeScript bilan yozganda har bo'lakka tur kerak. Zustand uni StateCreator beradi: StateCreator<ButunStore, Middlewarelar, [], ShuBo'lak>. Kanondagi ilova-holati.ts dan:

ts
// Slice (19/#22): har qism — o'z funksiyasi, store — ularning
// yig'indisi. Tur parametrlari — persist middleware ichida
// ishlashi uchun
type Qism<T> = StateCreator<
  IlovaHolati,
  [["zustand/persist", unknown]],
  [],
  T
>;

Birinchi parametr — butun store (get() butun holatni qaytarishi uchun). Ikkinchisi — bo'lak qaysi middleware'lar ichida ishlashi: bu yerda persist. Agar devtools ham bo'lsa — [["zustand/devtools", never], ["zustand/persist", unknown]]. Oxirgisi — shu bo'lak qaytaradigan qism. Bu turni bir marta yozib, keyin har bo'lak uchun Qism<ElonQismi>, Qism<SozlamaQismi> deysiz.

Tekshirib ko'ring: Slice misolida createGuestSlice dagi rename savat bo'lagidagi items ni ko'ra oladimi? Qanday?

Javob

Ha. createGuestSlice ga ham get keladi ((...a) — hamma argumentlar har bo'lakka). (set, get) => ({ rename: (g) => { console.log(get().items); … } }) — butun store'ni o'qiydi. Bo'laklar orasida devor yo'q — bu qulay, lekin ehtiyot bo'ling: bo'laklar bir-birining ichiga ko'p qarasa, ajratishdan foyda qolmaydi.

5. SSR tuzoqlari

SSR (Server-Side Rendering) — sahifani avval serverda HTML qilib chizish, keyin brauzerda uni jonlantirish (hydration). Kursda u Next.js bilan 23-qismda keladi. vazifalar da SSR yo'q, lekin bugungi store kodi u yerga ko'chganda ikki joyda qoqiladi. Hozir bilib qo'yamiz.

5.1 Birinchi tuzoq: umumiy store

Brauzerda modul bir marta yuklanadi va bitta mehmonga xizmat qiladi. Serverda esa Node jarayoni hamma so'rovlarga xizmat qiladi va modul ham bir marta yuklanadi. Modul darajasidagi create(...) — butun server uchun bitta store.

Tasavvur qiling: Malika savatga osh qo'shdi. Server Malikaning sahifasini chizish uchun umumiy store'ga yozdi. Shu payt Bobur sahifani ochdi — server unga ham o'sha store'dan chizdi. Bobur Malikaning savatini ko'rdi. Bu nafaqat xato, balki ma'lumot sizib chiqishi (data leak).

Davosi — har so'rovga alohida store: createStore (o'tgan darsdagi vanilla store) + Context. Store'ni Provider yaratadi va useRef/useState da saqlaydi. Natija oynasida ikki "so'rov" ni yonma-yon ko'rsatamiz:

jsx
import { createContext, use, useState } from "react";
import { createStore, useStore } from "zustand";

const createCartStore = (guest) =>
  createStore((set) => ({
    guest,
    items: [],
    add: (name) =>
      set((state) => ({ items: [...state.items, name] })),
  }));

const CartContext = createContext(null);

function CartProvider({ guest, children }) {
  // har Provider — o'z store'i (har so'rov, har mehmon)
  const [store] = useState(() => createCartStore(guest));
  return <CartContext value={store}>{children}</CartContext>;
}

function useCart(selector) {
  const store = use(CartContext);
  if (store === null) throw new Error("CartProvider yo'q");
  return useStore(store, selector);
}

function Cart() {
  const guest = useCart((state) => state.guest);
  const items = useCart((state) => state.items);
  const add = useCart((state) => state.add);
  return (
    <p>
      <button onClick={() => add("Osh")}>{guest}: +Osh</button>{" "}
      savat: {items.join(", ") || "bo'sh"}
    </p>
  );
}

export default function App() {
  return (
    <>
      <CartProvider guest="Malika">
        <Cart />
      </CartProvider>
      <CartProvider guest="Bobur">
        <Cart />
      </CartProvider>
    </>
  );
}

Ekranda:

text
Malika: +Osh savat: Osh
Bobur: +Osh savat: bo'sh

Malikaning bosishi Boburning savatiga tegmadi. useState(() => …) — dangasa boshlash: store faqat birinchi chizishda bir marta yaratiladi. use(CartContext) — use() darsidagi Context o'qish. Selektorlar va useShallow — o'sha-o'sha, faqat useStore(store, selektor) orqali.

Zustand'ning Next.js qo'llanmasi aynan shu naqshni beradi. Context bu yerda qiymatni emas, store havolasini tarqatadi. Havola o'zgarmaydi — Context sabab bo'lgan qayta chizish yo'q. Qayta chizishni yana selektor boshqaradi. Context va Zustand bir-birini to'ldirdi.

5.2 Ikkinchi tuzoq: hydration paytidagi birinchi kadr

Server mehmonning localStorage ini bilmaydi — u faqat brauzerda. Server sahifani standart sozlama bilan chizadi: "oddiy". Brauzerda esa persist "ixcham" ni o'qiydi. Jonlantirishda React serverdagi HTML'ni brauzerdagi birinchi chizish bilan solishtiradi. Ular farq qilsa — hydration xatosi.

Zustand 5 buni hisobga olgan. Natija oynasida serverdan kelgan HTML'ni qo'lda yozib, ustidan hydrateRoot qilamiz:

jsx
// mehmon kecha «ixcham» ni tanlagan
localStorage.setItem(
  "bahor:sozlamalar",
  JSON.stringify({ state: { compact: true }, version: 0 }),
);

import { hydrateRoot } from "react-dom/client";
import { create } from "zustand";
import { persist } from "zustand/middleware";

const useSettingsStore = create(
  persist(() => ({ compact: false }), { name: "bahor:sozlamalar" }),
);

function App() {
  const compact = useSettingsStore((state) => state.compact);
  console.log("chizildi:", compact ? "ixcham" : "oddiy");
  return <p>{`Menyu: ${compact ? "ixcham" : "oddiy"}`}</p>;
}

const s = useSettingsStore;
console.log("store'da:", s.getState().compact);
console.log("boshlang'ich:", s.getInitialState().compact);

// server yuborgan HTML (u localStorage'ni bilmaydi)
const root = document.getElementById("root");
root.innerHTML = "<p>Menyu: oddiy</p>";
hydrateRoot(root, <App />);

Konsolda:

text
store'da: true
boshlang'ich: false
chizildi: oddiy
chizildi: ixcham

Ekranda:

text
Menyu: ixcham

Xato yo'q, lekin ikki chizish bor. Sababi — useSyncExternalStore darsidagi uchinchi argument, getServerSnapshot. Zustand unga getInitialState() ni beradi — persist o'qishidan oldingi holat. Jonlantirish paytida React aynan shu bilan chizadi: "oddiy" — server HTML'iga mos. Darhol keyin haqiqiy holat bilan qayta chizadi: "ixcham".

Demak tuzoq — xato emas, miltillash: mehmon bir lahza "oddiy" menyuni ko'radi, keyin u "ixcham" ga sakraydi. Shunday ma'lumotni (tema, ixcham rejim) Next.js'da odatda serverga ham ma'lum qilishadi (cookie orqali), yoki birinchi kadrni maxsus yashirishadi. Bu 23-qism mavzusi.

Yana bir sozlama: persist(…, { skipHydration: true }) — persist o'zi o'qimaydi, siz useEffect ichida useSettingsStore.persist.rehydrate() ni chaqirasiz. Bu o'qish vaqtini to'liq o'zingiz boshqarishingiz kerak bo'lganda kerak.

vazifalar (Vite, SSR yo'q) Next.js (SSR)
store soni bitta, modul darajasida har so'rovga — Provider ichida
persist o'qishi store yaratilganda, sinxron brauzerda, jonlantirishdan keyin
birinchi kadr to'g'ri sozlama standart qiymat → miltillash

Tekshirib ko'ring: Nega vazifalar da Himoyalangan sahifadagi foydalanuvchi ma'lumoti bilan "boshqa mehmonning savati" muammosi chiqmaydi, garchi store modul darajasida bo'lsa ham?

Javob

vazifalar — faqat brauzerda ishlaydigan SPA. Har mehmonning brauzeri modulni o'zi yuklaydi va o'z store'iga ega. Umumiy store muammosi faqat bitta jarayon ko'p foydalanuvchiga sahifa chizganda chiqadi — serverda. Shuning uchun kanon modul darajasidagi create ni ishlatadi va bu to'g'ri.

6. Vazifalar qadami: sozlamalar — store'da, commit (holat 22)

6.1 Nima quramiz

O'tgan darsda e'lonni store'ga ko'chirdik. Bugun ikki foydalanuvchi sozlamasi:

  • «O'chirishdan oldin so'rash» — o'chirish dialogi (dialog). Ko'p vazifani tez tozalaydigan foydalanuvchi uni o'chirib qo'yishi mumkin.
  • «Ixcham ro'yxat» — qatorlar orasidagi bo'shliq kichikroq.

Ikkalasi persist bilan saqlanadi, e'lon esa yo'q. Sozlamalar sahifasi (React Router I da yaratilgan) ularni o'zgartiradi, ro'yxat sahifasi o'qiydi.

6.2 To'liq store: ilova-holati.ts

O'tgan darsdagi e'lon qismiga sozlama qismi va persist qo'shiladi. Kanondan (o'tgan darsda ko'rilgan bosh izoh va e'lon qismi o'rniga …):

ts
import { create } from "zustand";
import type { StateCreator } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";

type Zichlik = "oddiy" | "ixcham";

type SozlamaQismi = {
  /** o'chirishdan oldin dialog (13/#09) — o'chirib qo'ysa
   *  bo'ladi */
  readonly ochirishdaSora: boolean;
  readonly zichlik: Zichlik;
  setOchirishdaSora: (qiymat: boolean) => void;
  setZichlik: (qiymat: Zichlik) => void;
};

export type IlovaHolati = ElonQismi & SozlamaQismi;

// … Qism<T> turi (yuqorida), ELON_MUDDATI, elonQismi …

const sozlamaQismi: Qism<SozlamaQismi> = (set) => ({
  ochirishdaSora: true,
  zichlik: "oddiy",
  setOchirishdaSora: (qiymat) => set({ ochirishdaSora: qiymat }),
  setZichlik: (qiymat) => set({ zichlik: qiymat }),
});

export const SOZLAMALAR_KALITI = "vazifalar:sozlamalar";

/**
 * persist (19/#22): sozlamalar localStorage'da — faqat ular
 * (partialize), e'lon emas. version — shakl o'zgarsa, eski yozuvni
 * ko'chirish (migrate) uchun. Saqlangan qiymat store yaratilganda
 * SINXRON o'qiladi — birinchi kadrdayoq to'g'ri sozlama (SSR'da
 * bunday emas — #22 tuzog'i)
 */
export const useIlovaHolati = create<IlovaHolati>()(
  persist((...a) => ({ ...elonQismi(...a), ...sozlamaQismi(...a) }), {
    name: SOZLAMALAR_KALITI,
    version: 1,
    storage: createJSONStorage(() => localStorage),
    partialize: (h) => ({
      ochirishdaSora: h.ochirishdaSora,
      zichlik: h.zichlik,
    }),
  }),
);

// … storage hodisasi → rehydrate() («Boshqa tab» bo'limida) …

Bugungi darsning hamma qismi bitta faylda: ikki slice (elonQismi, sozlamaQismi) — Qism<T> turi bilan; create<IlovaHolati>() — ikki qavs; persist — name, version, storage, partialize; oxirida boshqa tab uchun storage hodisasi.

Kalit nomi vazifalar:sozlamalar — 13-qismdan beri ishlatilayotgan vazifalar: prefiksi bilan (localStorage darsidagi kelishuv). version: 1 — migrate hali yo'q, chunki eski yozuv yo'q. Kelajakda shakl o'zgarsa, version: 2 va migrate qo'shiladi. devtools va immer kanonda yo'q: store kichik va tekis — immer kerak emas, devtools o'rniga React DevTools yetadi (u hook qiymatini ko'rsatadi).

6.3 Sozlamalar sahifasi

SozlamalarSahifasi.tsx ga ikki katakcha (kanon):

tsx
  const ochirishdaSora = useIlovaHolati((h) => h.ochirishdaSora);
  const zichlik = useIlovaHolati((h) => h.zichlik);
  const setOchirishdaSora = useIlovaHolati((h) => h.setOchirishdaSora);
  const setZichlik = useIlovaHolati((h) => h.setZichlik);
  return (
    <>
      <title>Sozlamalar — Vazifalar</title>
      <h2 ref={sarlavhaRef} tabIndex={-1}>
        Sozlamalar
      </h2>
      <section id="sozlamalar" aria-labelledby={sarlavhaId}>
        <h3 id={sarlavhaId}>Ro'yxat</h3>
        <p>
          <input
            type="checkbox"
            id="ochirishda-sora"
            checked={ochirishdaSora}
            onChange={(event) =>
              setOchirishdaSora(event.target.checked)
            }
          />{" "}
          <label htmlFor="ochirishda-sora">
            O'chirishdan oldin so'rash
          </label>
        </p>
        {/* «Ixcham ro'yxat» — xuddi shunday, setZichlik bilan */}
      </section>

To'rt selektor — har biri bitta qiymat yoki funksiya (o'tgan darsdagi qoida). <section aria-labelledby> — Malika (ekran o'quvchi) bo'limni sarlavhasi bilan eshitadi (useId bilan bog'langan).

6.4 Ro'yxat sozlamani o'qiydi

App.tsx da ikki selektor va ikki o'zgarish (kanon):

diff
+  // 19/#22: foydalanuvchi sozlamalari (Sozlamalar sahifasi)
+  const ochirishdaSora = useIlovaHolati((h) => h.ochirishdaSora);
+  const zichlik = useIlovaHolati((h) => h.zichlik);
 ...
     const zaxira = qoshniId(korinadigan, id);
+    // 19/#22: sozlamada "so'ramasin" tanlangan — darhol
+    if (!ochirishdaSora) {
+      ochir({ id, matn: vazifa.matn, zaxira });
+      return;
+    }
 ...
-          className={eskirgan ? "eskirgan" : undefined}
+          className={
+            [eskirgan && "eskirgan", zichlik === "ixcham" && "ixcham"]
+              .filter(Boolean)
+              .join(" ") || undefined
+          }

Dialog tasdiqlagan o'chirish va "so'ramasdan" o'chirish endi bitta ochir() funksiyasidan o'tadi — kod takrorlanmasin. CSS'ga 4 qator: #royxat.ixcham li ning ichki bo'shlig'i kichikroq.

6.5 Testlar va tekshiruvlar

Yangi IlovaHolati.test.tsx — store'ning o'zi, React'siz (kanon, bitta test):

tsx
  test("persist: sozlamalar localStorage'da, e'lon — yo'q (partialize)", () => {
    useIlovaHolati.getState().setOchirishdaSora(false);
    useIlovaHolati.getState().elonQil("Saqlanmasin");
    const saqlangan = JSON.parse(
      localStorage.getItem(SOZLAMALAR_KALITI) ?? "null",
    ) as unknown;
    expect(saqlangan).toEqual({
      state: { ochirishdaSora: false, zichlik: "oddiy" },
      version: 1,
    });
  });

Qolgan ikki test: e'lon 3 soniyada o'chishi (soxta taymer bilan) va boshqa tab (StorageEvent yuboriladi, zichlik "ixcham" bo'lishi kutiladi). Har testdan keyin afterEach sozlamalarni boshlang'ich qiymatga qaytaradi — o'tgan darsdagi "testlar bir-biriga ta'sir qilmasin" qoidasi.

bash
git add -A
git commit

Commit (kanon): feat: e'lon va sozlamalar — Zustand store (slice, persist), sahifalararo e'lon.

Tekshiruvlar (haqiqiy, holat 22):

  • npm test — node:test ℹ tests 177 · ℹ pass 177, Vitest Tests 59 passed (59) (+3 store testi). «Orqaga» tuzatishidan (React Router III) keyin kanonda — node:test 185. lint, format:check, tip, knip — exit 0.
  • Build: index-….js 421.00 kB │ gzip: 132.77 kB (holat 18 da 131.31). Zustand va yangi kod — taxminan +1.4 KB gzip; kutubxonaning o'zi — +1.3 KB.
  • Brauzer: 13–17-qism tekshiruvlari 159/159, 18-qismniki 72/72, 19-qismniki 72/72 («Orqaga» va /kirish tuzatishlaridan keyin 75). Yangi yettitasi:
text
✅ action'dan e'lon (getState) — ro'yxat sahifasida ko'rindi: "«Kitob o'qish» o'chirildi."
✅ sozlamalar: localStorage'da faqat ikki sozlama (persist, partialize): {"state":{"ochirishdaSora":false,"zichlik":"ixcham"},"version":1}
✅ boshqa tab: sozlama o'zgarishi yetib keldi (storage → rehydrate): false
✅ ixcham ro'yxat — class: "ixcham"
✅ so'ramasdan o'chirildi: dialog ochilmadi, e'lon, serverda yo'q: [false,"«Non olish #bozor» o'chirildi.",[2]]
✅ qayta ochilganda sozlama saqlangan (birinchi kadrdayoq): "ixcham"
✅ Zustand: CSP buzilishi yo'q: []

Uchinchi qator — ikki tab: birinchisida «O'chirishdan oldin so'rash» o'chirildi, ikkinchisi storage hodisasi orqali false ni oldi. Oltinchi qator — sahifa qayta ochilganda birinchi kadrdayoq "ixcham" (SSR yo'q — persist sinxron). Oxirgisi — CSP ostida Zustand hech qanday taqiqlangan ish qilmadi (eval yo'q). Diff (holat 18 dan): 11 fayl, +296 −46.

7. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
sozlama yangilanishdan keyin yo'qoladi persist ulanmagan yoki name har safar boshqa bitta doimiy name
localStorage da keraksiz (e'lon, qoralama) yozuv partialize yo'q — hammasi saqlanadi partialize: (s) => ({ … })
shakl o'zgargach eski mehmonlarda ilova yiqiladi eski yozuv yangi kod bilan o'qildi version ni oshiring + migrate
State loaded from storage couldn't be migrated since no migrate function was provided version oshdi, migrate yo'q — sozlamalar tashlandi har versiya o'zgarishiga migrate
boshqa tabdagi o'zgarish ko'rinmaydi persist faqat yaratilganda o'qiydi storage hodisasi → persist.rehydrate()
immer bilan push qilindi, lekin ekran eski immer middleware ulanmagan immer(...) bilan o'rang yoki yangi massiv
TypeScript'da persist qo'shilgach tur xatolari create<T>(…) — bitta qavs create<T>()(…)
Next.js'da bir mehmon boshqasining ma'lumotini ko'radi modul darajasidagi store serverda umumiy har so'rovga createStore + Context

8. Mashqlar

Mashqlar kurs/mashqlar/19/22-zustand-middleware/ da. Har yechimni «Ishga tushir» bilan sinang.

1-mashq (oson): faqat tema saqlansin

Store'da theme va search bor. Qidiruv matni saqlanmasligi kerak. Bo'sh joyni to'ldiring:

jsx
persist(yaratuvchi, {
  name: "bahor:menyu",
  [___:partialize]: (state) => ({ theme: state.theme }),
});
Yechim
jsx
import { create } from "zustand";
import { persist } from "zustand/middleware";

const useMenuStore = create(
  persist(
    (set) => ({
      theme: "oq",
      search: "",
      setTheme: (theme) => set({ theme }),
      setSearch: (search) => set({ search }),
    }),
    {
      name: "bahor:menyu",
      partialize: (state) => ({ theme: state.theme }),
    },
  ),
);

export default function App() {
  const setTheme = useMenuStore((state) => state.setTheme);
  const setSearch = useMenuStore((state) => state.setSearch);
  return (
    <>
      <input
        aria-label="Qidiruv"
        onChange={(event) => setSearch(event.target.value)}
      />
      <button onClick={() => setTheme("qora")}>Qora tema</button>{" "}
      <button onClick={showStorage}>Xotira</button>
    </>
  );
}

function showStorage() {
  const { search } = useMenuStore.getState();
  console.log("store'da:", search);
  console.log(localStorage.getItem("bahor:menyu"));
}

Konsolda:

text
store'da: osh
{"state":{"theme":"qora"},"version":0}

Qidiruv matni store'da bor, xotirada — yo'q: persist har set dan keyin yozdi, lekin faqat partialize qaytargan theme ni. showStorage — oddiy funksiya, shuning uchun getState() bilan o'qiydi (o'tgan darsdagi qoida).

2-mashq (o'rta): lang → language

«Bahor» saytining eski versiyasi tilni { lang: "uz" } deb saqlagan (versiya 0). Yangi kodda maydon nomi language. Boshlang'ich kodda eski yozuv tayyor. version va migrate qo'shing, shunda ekranda Til: uz chiqsin (hozir Til: en — standart qiymat, mehmonning tanlovi yo'qoldi).

jsx
localStorage.setItem(
  "bahor:til",
  JSON.stringify({ state: { lang: "uz" }, version: 0 }),
);

import { create } from "zustand";
import { persist } from "zustand/middleware";

const useLangStore = create(
  persist(() => ({ language: "en" }), { name: "bahor:til" }),
);

export default function App() {
  const language = useLangStore((state) => state.language);
  return <p>Til: {language}</p>;
}

Ekranda:

text
Til: en
Yechim
jsx
localStorage.setItem(
  "bahor:til",
  JSON.stringify({ state: { lang: "uz" }, version: 0 }),
);

import { create } from "zustand";
import { persist } from "zustand/middleware";

const useLangStore = create(
  persist(() => ({ language: "en" }), {
    name: "bahor:til",
    version: 1,
    migrate: (old, version) => {
      if (version < 1) {
        return { language: old.lang };
      }
      return old;
    },
  }),
);

export default function App() {
  const language = useLangStore((state) => state.language);
  return <p>Til: {language}</p>;
}

console.log(localStorage.getItem("bahor:til"));

Konsolda:

text
{"state":{"language":"uz"},"version":1}

Ekranda:

text
Til: uz

Boshlang'ich kodda nega en edi? Saqlangan { lang: "uz" } boshlang'ich { language: "en" } bilan birlashtirildi. Natijada ikkala maydon ham bor edi, lekin komponent language ni o'qidi. migrate eski maydonni yangi nomga ko'chiradi va yozuvni darhol 1-versiya bilan qayta saqlaydi.

3-mashq (qiyin): ikki slice va TypeScript'siz tur

Store'ni ikki bo'lakdan yig'ing:

  1. createOrderSlice — items (taom nomlari), add(name), submit(). submit savat bo'sh bo'lmasa konsolga <stol>-stol: <taomlar> yozadi va savatni tozalaydi.
  2. createTableSlice — table (boshida 4), nextTable() — stol raqami +1.
  3. submit stol raqamini boshqa bo'lakdan get() bilan oladi.

Tugmalar: «Osh», «Keyingi stol», «Yuborish». Ishora: «Store'ni bo'laklarga bo'lish» bo'limidagi misol.

Yechim
jsx
import { create } from "zustand";

const createOrderSlice = (set, get) => ({
  items: [],
  add: (name) =>
    set((state) => ({ items: [...state.items, name] })),
  submit: () => {
    const { items, table } = get();
    if (items.length === 0) return;
    console.log(`${table}-stol: ${items.join(", ")}`);
    set({ items: [] });
  },
});

const createTableSlice = (set) => ({
  table: 4,
  nextTable: () => set((state) => ({ table: state.table + 1 })),
});

const useOrderStore = create((...a) => ({
  ...createOrderSlice(...a),
  ...createTableSlice(...a),
}));

export default function App() {
  const table = useOrderStore((state) => state.table);
  const add = useOrderStore((state) => state.add);
  const next = useOrderStore((state) => state.nextTable);
  const submit = useOrderStore((state) => state.submit);
  return (
    <p>
      {table}-stol:{" "}
      <button onClick={() => add("Osh")}>Osh</button>{" "}
      <button onClick={next}>Keyingi stol</button>{" "}
      <button onClick={submit}>Yuborish</button>
    </p>
  );
}

Konsolda:

text
4-stol: Osh
5-stol: Osh, Osh

Ekranda:

text
5-stol: Osh Keyingi stol Yuborish

submit buyurtma bo'lagida, table — stol bo'lagida, lekin get() ikkalasini birga beradi: store bitta.

4-mashq: Vazifalar qadami

feat/zustand branch'ida «Vazifalar qadami» bo'limidagi o'zgarishlarni yakunlang: ilova-holati.ts (slice'lar, persist, storage hodisasi), Sozlamalar sahifasidagi ikki katakcha, App.tsx dagi ochirishdaSora va zichlik, CSS, IlovaHolati.test.tsx. Keyin commit.

Yechim

Tekshiruv ro'yxati:

  • npm test — node:test 185 («Orqaga» tuzatishigacha — 177), Vitest 59 (hammasi pass).
  • npm run dev — Sozlamalarda «Ixcham ro'yxat» ni yoqing, ro'yxatga qayting: qatorlar zichroq. Sahifani yangilang — sozlama saqlangan, miltillash yo'q.
  • DevTools → Application → Local Storage: vazifalar:sozlamalar — {"state":{…},"version":1}, e'lon yo'q.
  • Saytni ikkinchi tabda oching, birinchisida sozlamani o'zgartiring — ikkinchi tabga o'tsangiz, Sozlamalar sahifasida yangi qiymat.
  • «O'chirishdan oldin so'rash» o'chiq bo'lsa — o'chirish dialogsiz, e'lon bilan.
  • git log --oneline -1 — feat: e'lon va sozlamalar — Zustand store (slice, persist), sahifalararo e'lon.

9. Real ishda

  • persist — savat, tema, til, "yangiliklarni ko'rdim" belgilari, qoralama formalar. Katta ma'lumot (minglab yozuv, rasm) uchun — IndexedDB (keyingi darslardan birida).
  • devtools — jamoaviy loyihalarda deyarli majburiy: "bu qiymat qayerdan keldi?" savoliga jurnal javob beradi. Amal nomlari (savat/qoshish) ni izchil yozish — kod ko'rigida so'raladi.
  • Next.js — Zustand'ning rasmiy qo'llanmasi "har so'rovga store, Context orqali" naqshini beradi. Server komponentlarida (Server Components) store umuman ishlatilmaydi — u faqat klient komponentlar uchun.
  • Intervyu savollari: "persist'da eski ma'lumotni qanday ko'chirasiz?", "nega create<T>()()?", "Next.js'da Zustand store'ni qanday yaratasiz va nega global emas?", "middleware tartibi nimaga ta'sir qiladi?".

Xulosa

  • Middleware — store yaratuvchini o'rab, imkoniyat qo'shadi: persist, devtools, immer; tartib — devtools tashqarida, immer ichkarida.
  • persist: name — kalit, partialize — nimani saqlash, version + migrate — eski yozuvni ko'chirish, boshqa tab — storage hodisasi va rehydrate().
  • immer — chuqur holatni "o'zgartirib" yozish; natija baribir yangi va muzlatilgan obyekt.
  • Slice — bitta store'ni fayllarga bo'lish; get() butun holatni beradi. TypeScript'da — create<T>()(…) va StateCreator.
  • SSR: serverda modul darajasidagi store umumiy → har so'rovga createStore + Context; jonlantirishda birinchi kadr — standart qiymat (miltillash).
  • vazifalar: e'lon va ikki sozlama — bitta store, persist faqat sozlamalarni saqlaydi; commit, holat 22.

Keyingi dars: Redux Toolkit I: store, slice va hooklar — Flux oqimi, configureStore, createSlice, useSelector/useDispatch; Redux qaysi loyihada to'g'ri tanlov va biz uni nega kanonga olmadik.

Manbalar

  • Zustand: Persisting store data (partialize, version, migrate, skipHydration) — zustand.docs.pmnd.rs/integrations/persisting-store-data (v5)
  • Zustand: Middlewares — devtools, immer — zustand.docs.pmnd.rs/middlewares
  • Zustand: Slices pattern; TypeScript guide (create<T>()) — zustand.docs.pmnd.rs/guides/slices-pattern, /guides/typescript
  • Zustand: Setup with Next.js — zustand.docs.pmnd.rs/guides/nextjs
  • React: hydrateRoot, useSyncExternalStore (getServerSnapshot) — react.dev/reference
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Zustand middleware: persist, devtools, immer, slice'lar va SSR tuzoqlari — IlmHamroh