Zustand — async action, middleware, devtools

~20 daq · React · State Boshqarish

Muammodan boshlaylik

57-darsdagi savat ishlaydi. Lekin:

  1. Sahifani yangilasangiz — savat bo'shab qoladi. Foydalanuvchi 10 ta mahsulot tanlagan edi
  2. Savatni rasmiylashtirish — serverga so'rov. Action async bo'lishi kerak
  3. Savat qandaydir noto'g'ri holatga tushdi — qaysi action, qachon, qanday o'zgartirdi — ko'rinmaydi
  4. Ichma-ich obyektlarni yangilash uchun ...spread lar zanjiri o'qib bo'lmaydigan bo'lib ketdi

Zustand bularni middleware lar bilan hal qiladi: store ni o'rab, unga yangi imkoniyat qo'shadigan funksiyalar.


Async action lar

Zustand da async action uchun maxsus narsa kerak emas — oddiy async funksiya:

export const useBuyurtmaStore = create((set, get) => ({
  holat: 'idle',            // 'idle' | 'yuborilmoqda' | 'muvaffaqiyat' | 'xato'
  xato: null,

  rasmiylashtirish: async () => {
    set({ holat: 'yuborilmoqda', xato: null });
    try {
      const mahsulotlar = useSavatStore.getState().mahsulotlar;
      const j = await fetch('/api/buyurtmalar', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ mahsulotlar }),
      });
      if (!j.ok) throw new Error('Buyurtma qabul qilinmadi');

      useSavatStore.getState().tozalash();
      set({ holat: 'muvaffaqiyat' });
    } catch (err) {
      set({ holat: 'xato', xato: err.message });
    }
  },
}));
function Rasmiylashtirish() {
  const holat = useBuyurtmaStore((s) => s.holat);
  const rasmiylashtirish = useBuyurtmaStore((s) => s.rasmiylashtirish);

  return (
    <button onClick={rasmiylashtirish} disabled={holat === 'yuborilmoqda'}>
      {holat === 'yuborilmoqda' ? 'Yuborilmoqda...' : 'Buyurtma berish'}
    </button>
  );
}

set ni await dan oldin ham, keyin ham chaqirish mumkin. Boshqa store ga getState() orqali murojaat qilinadi.

[!INFO] Server bilan ishlaydigan async action lar uchun avval TanStack Query ni o'ylang (56-dars): useMutation yuklanish va xato holatlarini, qayta urinishni va kesh yangilashni o'zi qiladi. Zustand dagi async action — server ma'lumoti global client state ga ta'sir qilganda mos (masalan, login dan keyin token ni saqlash).


persist — localStorage ga saqlash

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

export const useSavatStore = create(
  persist(
    (set) => ({
      mahsulotlar: [],
      qoshish: (m) => set((s) => ({ mahsulotlar: [...s.mahsulotlar, m] })),
      tozalash: () => set({ mahsulotlar: [] }),
    }),
    {
      name: 'savat',            // localStorage dagi kalit
    }
  )
);

Hammasi. Har bir o'zgarish localStorage ga yoziladi, sahifa ochilganda qaytariladi. 1-muammo hal.

Sozlamalar

import { persist, createJSONStorage } from 'zustand/middleware';

persist(
  (set) => ({ ... }),
  {
    name: 'savat',
    storage: createJSONStorage(() => sessionStorage),   // tab yopilguncha
    partialize: (state) => ({ mahsulotlar: state.mahsulotlar }),  // faqat shu qismni saqlash
    version: 2,                                          // tuzilma o'zgarsa
    migrate: (eski, versiya) => {
      if (versiya < 2) {
        return { mahsulotlar: eski.items ?? [] };       // eski nomni yangisiga
      }
      return eski;
    },
  }
)
  • partialize — vaqtinchalik holatlarni (yuklanmoqda, xato) saqlamaslik uchun
  • version + migrate — store tuzilmasi o'zgarganda eski foydalanuvchilarning saqlangan ma'lumotini moslashtirish

[!WARNING] localStorage ga maxfiy ma'lumot (parol, karta raqami) saqlamang — sahifadagi har qanday JavaScript (shu jumladan zararli kengaytma yoki XSS) uni o'qiy oladi. Auth token larni saqlash ham xavfsizlik nuqtai nazaridan bahsli; ko'p loyihalarda httpOnly cookie afzal ko'riladi.


devtools — Redux DevTools bilan

import { devtools } from 'zustand/middleware';

export const useSavatStore = create(
  devtools(
    (set) => ({
      mahsulotlar: [],
      qoshish: (m) =>
        set(
          (s) => ({ mahsulotlar: [...s.mahsulotlar, m] }),
          false,
          'savat/qoshish'          // DevTools da action nomi
        ),
    }),
    { name: 'SavatStore' }
  )
);

Brauzerga Redux DevTools kengaytmasini o'rnating. Endi:

  • Har bir o'zgarish nomi va vaqti bilan ro'yxatda
  • Har qadamdagi state ni ko'rish
  • "Vaqt bo'ylab sayohat" — oldingi holatga qaytish

set ning uchinchi argumenti — action nomi. Busiz DevTools da hammasi "anonymous" bo'lib ko'rinadi. 3-muammo hal.


immer — ichma-ich yangilash

Chuqur ichma-ich state ni yangilash:

// ❌ spread zanjiri
set((s) => ({
  foydalanuvchi: {
    ...s.foydalanuvchi,
    manzil: {
      ...s.foydalanuvchi.manzil,
      shahar: 'Samarqand',
    },
  },
}));

immer middleware i bilan — xuddi o'zgartirayotgandek yozasiz, lekin natija baribir yangi obyekt:

npm install immer
import { immer } from 'zustand/middleware/immer';

export const useProfilStore = create(
  immer((set) => ({
    foydalanuvchi: { ism: 'Ali', manzil: { shahar: 'Toshkent' } },

    shaharniOzgartir: (shahar) =>
      set((state) => {
        state.foydalanuvchi.manzil.shahar = shahar;   // to'g'ridan-to'g'ri!
      }),
  }))
);

Immer "qoralama" (draft) yaratadi, siz uni o'zgartirasiz, u esa o'zgarishlar asosida yangi o'zgarmas obyekt yasaydi. 4-muammo hal.


Middleware larni birlashtirish

import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';

export const useSavatStore = create(
  devtools(
    persist(
      immer((set) => ({
        mahsulotlar: [],
        qoshish: (m) =>
          set((s) => {
            const bor = s.mahsulotlar.find((x) => x.id === m.id);
            if (bor) bor.soni += 1;
            else s.mahsulotlar.push({ ...m, soni: 1 });
          }),
        ochirish: (id) =>
          set((s) => {
            s.mahsulotlar = s.mahsulotlar.filter((x) => x.id !== id);
          }),
      })),
      { name: 'savat' }
    ),
    { name: 'SavatStore' }
  )
);

Tavsiya etilgan tartib: devtools eng tashqarida — u barcha o'zgarishlarni ko'rishi uchun; immer eng ichkarida.

[!TIP] persist ishlatilganda server da (SSR — Next.js) va brauzerda boshlang'ich state farq qilishi mumkin: server bo'sh savat bilan render qiladi, brauzer esa localStorage dan to'ldiradi — "hydration mismatch" ogohlantirishi. Oddiy Vite + React ilovada bu muammo yo'q.


Slice pattern — katta store ni bo'lish

Store katta bo'lib ketsa, uni mantiqiy qismlarga ajratish mumkin:

// store/slices/savatSlice.js
export const savatSlice = (set) => ({
  mahsulotlar: [],
  qoshish: (m) => set((s) => ({ mahsulotlar: [...s.mahsulotlar, m] })),
});

// store/slices/uiSlice.js
export const uiSlice = (set) => ({
  yonPanelOchiq: false,
  yonPanelniAlmashtir: () => set((s) => ({ yonPanelOchiq: !s.yonPanelOchiq })),
});

// store/index.js
export const useStore = create((...a) => ({
  ...savatSlice(...a),
  ...uiSlice(...a),
}));

Lekin ko'p hollarda alohida store lar (57-dars) soddaroq va yetarli.


Nima uchun shunday?

Nima uchun Zustand bu imkoniyatlarni o'ziga qurmasdan, alohida middleware sifatida beradi?

Chunki har bir loyihaga hammasi kerak emas. persist — faqat saqlanishi kerak bo'lgan store larga, immer — faqat chuqur ichma-ich state ga, devtools — faqat development da. Middleware — bu oddiy funksiya kompozitsiyasi: har biri store yaratuvchi funksiyani oladi va yangi, kengaytirilgan funksiya qaytaradi. Bu yondashuv kutubxonani kichik saqlaydi (keraksiz kod bundle ga kirmaydi) va sizga o'z middleware ingizni yozish imkonini beradi — masalan, har bir o'zgarishni analitikaga yuboradigan. Zustand ning falsafasi: minimal yadro, kerakli narsani o'zingiz qo'shasiz.


Xulosa

Eslab qoling:

  • Async action — oddiy async funksiya; await dan oldin va keyin set
  • Server ma'lumotlari uchun avval TanStack Query ni o'ylang
  • persist — localStorage; name, partialize, version + migrate
  • Maxfiy ma'lumot localStorage ga emas
  • devtools — Redux DevTools; set ning 3-argumenti — action nomi
  • immer — ichma-ich state ni "to'g'ridan-to'g'ri" yangilash
  • Tartib: devtools(persist(immer(...)))
  • Katta store — slice pattern yoki alohida store lar

Keyingi dars

59-dars: Redux Toolkit — nima, Zustand dan farqi, qachon kerak