Zustand — global holat (savat, sozlamalar)

~15 daq · React Native · Ma'lumot va API

Muammodan boshlaylik

Savat:

  • Mahsulot ekranidagi "Savatga qo'shish" tugmasi uni o'zgartiradi
  • Tab panel badge si mahsulotlar sonini ko'rsatadi
  • Savat ekrani ro'yxatni va jami summani ko'rsatadi
  • Katalogdagi har bir kartada "savatda: 2" belgisi

To'rtta turli joy — bitta ma'lumot. Props orqali uzatish — _layout dan to'rt qavat pastga. Context — har bir o'zgarishda uni ishlatadigan barcha komponentlar qayta chiziladi.

Kerak — oddiy, tez global holat. Zustand — eng ixcham yechimlardan biri.


O'rnatish va store

npm install zustand
// stores/savat.ts
import { create } from 'zustand';

type SavatQatori = { id: number; nom: string; narx: number; rasm: string; soni: number };

type SavatHolati = {
  qatorlar: SavatQatori[];
  qosh: (m: Omit<SavatQatori, 'soni'>) => void;
  kamaytir: (id: number) => void;
  ochir: (id: number) => void;
  tozala: () => void;
};

export const useSavat = create<SavatHolati>()((set) => ({
  qatorlar: [],

  qosh: (m) =>
    set((s) => {
      const bor = s.qatorlar.find((q) => q.id === m.id);
      return {
        qatorlar: bor
          ? s.qatorlar.map((q) => (q.id === m.id ? { ...q, soni: q.soni + 1 } : q))
          : [...s.qatorlar, { ...m, soni: 1 }],
      };
    }),

  kamaytir: (id) =>
    set((s) => ({
      qatorlar: s.qatorlar
        .map((q) => (q.id === id ? { ...q, soni: q.soni - 1 } : q))
        .filter((q) => q.soni > 0),
    })),

  ochir: (id) => set((s) => ({ qatorlar: s.qatorlar.filter((q) => q.id !== id) })),

  tozala: () => set({ qatorlar: [] }),
}));
  • create — store yaratadi va hook qaytaradi
  • set — holatni yangilash; qaytarilgan ob'ekt mavjud holat bilan birlashtiriladi
  • Provider kerak emas — hook istalgan joyda ishlaydi

Ishlatish — selektorlar bilan

// Mahsulot ekrani
const qosh = useSavat((s) => s.qosh);

<Tugma matn="Savatga qo'shish" onPress={() => qosh({ id: m.id, nom: m.nom, narx: m.narx, rasm: m.rasm })} />
// Tab badge
const soni = useSavat((s) => s.qatorlar.reduce((j, q) => j + q.soni, 0));

<Tabs.Screen name="savat" options={{ tabBarBadge: soni || undefined }} />
// Katalog kartasi
const savatdagi = useSavat((s) => s.qatorlar.find((q) => q.id === mahsulot.id)?.soni ?? 0);

Selektor — useSavat((s) => s.xxx) — komponent faqat o'ziga kerakli qism o'zgarganda qayta chiziladi. Tab badge faqat son o'zgarganda; boshqa mahsulot kartasi — faqat o'z mahsuloti soni o'zgarganda.

[!WARNING] const savat = useSavat() — selektorsiz — butun store ni oladi va har qanday o'zgarishda qayta chiziladi. Doim selektor ishlating.

Bir nechta qiymat

import { useShallow } from 'zustand/react/shallow';

const { qatorlar, kamaytir, ochir } = useSavat(
  useShallow((s) => ({ qatorlar: s.qatorlar, kamaytir: s.kamaytir, ochir: s.ochir }))
);

useShallow — ob'ekt har safar yangi yaratilgani uchun cheksiz qayta chizishning oldini oladi.


Savat ekrani

export default function SavatEkran() {
  const qatorlar = useSavat((s) => s.qatorlar);
  const { qosh, kamaytir, ochir } = useSavat(useShallow((s) => ({ qosh: s.qosh, kamaytir: s.kamaytir, ochir: s.ochir })));
  const jami = qatorlar.reduce((j, q) => j + q.narx * q.soni, 0);

  if (qatorlar.length === 0) {
    return <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}><Text>Savat bo'sh</Text></View>;
  }

  return (
    <View style={{ flex: 1 }}>
      <FlatList
        data={qatorlar}
        keyExtractor={(q) => String(q.id)}
        renderItem={({ item }) => (
          <View style={{ flexDirection: 'row', alignItems: 'center', padding: 12, gap: 12 }}>
            <Image source={{ uri: item.rasm }} style={{ width: 56, height: 56, borderRadius: 8 }} />
            <View style={{ flex: 1 }}>
              <Text numberOfLines={1}>{item.nom}</Text>
              <Text style={{ fontWeight: '700' }}>{(item.narx * item.soni).toLocaleString()} so'm</Text>
            </View>
            <Pressable onPress={() => kamaytir(item.id)} hitSlop={8}><Ionicons name="remove-circle-outline" size={26} /></Pressable>
            <Text>{item.soni}</Text>
            <Pressable onPress={() => qosh(item)} hitSlop={8}><Ionicons name="add-circle-outline" size={26} /></Pressable>
          </View>
        )}
      />
      <View style={{ padding: 16, borderTopWidth: 1, borderColor: '#e2e8f0' }}>
        <Text style={{ fontSize: 18, fontWeight: '700' }}>Jami: {jami.toLocaleString()} so'm</Text>
        <Tugma matn="Rasmiylashtirish" onPress={() => router.push('/rasmiylashtirish')} />
      </View>
    </View>
  );
}

Komponentdan tashqarida

// Oddiy funksiyada (masalan, API javobidan keyin)
useSavat.getState().tozala();
const hozirgi = useSavat.getState().qatorlar;

Buyurtma muvaffaqiyatli — savatni tozalash, useMutation ning onSuccess ida.


Saqlash: persist

Ilova yopilsa — savat yo'qoladi. persist middleware — avtomatik saqlash:

npx expo install @react-native-async-storage/async-storage
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';

export const useSavat = create<SavatHolati>()(
  persist(
    (set) => ({
      // ... yuqoridagi barcha kod
    }),
    {
      name: 'savat',
      storage: createJSONStorage(() => AsyncStorage),
      partialize: (s) => ({ qatorlar: s.qatorlar }),   // faqat ma'lumotni saqlash
    }
  )
);

Ilovani yopib ochsangiz — savat joyida. AsyncStorage haqida — keyingi darsda.


Sozlamalar store

// stores/sozlamalar.ts
type Sozlamalar = {
  mavzu: 'tizim' | 'yorug' | 'qorongi';
  til: 'uz' | 'ru' | 'en';
  bildirishnoma: boolean;
  ozgartir: (o: Partial<Omit<Sozlamalar, 'ozgartir'>>) => void;
};

export const useSozlamalar = create<Sozlamalar>()(
  persist(
    (set) => ({
      mavzu: 'tizim',
      til: 'uz',
      bildirishnoma: true,
      ozgartir: (o) => set(o),
    }),
    { name: 'sozlamalar', storage: createJSONStorage(() => AsyncStorage) }
  )
);

Qaysi holat qayerda

Holat turi Misol Vosita
Server holati Mahsulotlar, profil, buyurtmalar TanStack Query (32)
Global klient holati Savat, sozlamalar, mavzu Zustand
Lokal holat Modal ochiqmi, input qiymati, tanlangan tab useState
Forma holati Ro'yxatdan o'tish maydonlari React Hook Form (21)
URL holati Filtr, tanlangan ID Router parametrlari (29)
Autentifikatsiya Kirgan foydalanuvchi Context yoki Zustand (30)

Eng ko'p xato — hammasini bitta global store ga tiqish. Har bir turni o'z joyida saqlang.


Nima uchun shunday?

Nima uchun Zustand, Redux yoki Context emas?

Context — qiymatni uzatish uchun yaxshi, lekin u o'zgarganda barcha iste'molchilar qayta chiziladi — tez-tez o'zgaradigan savat uchun bu sekin. Redux — kuchli va qat'iy, lekin oddiy savat uchun ko'p tantanali kod (actions, reducers, slices). Zustand — o'rtadagi yechim: bitta funksiya bilan store, selektorlar orqali aniq qayta chizish, provider siz. Kichik va o'rta ilovalarning ko'pchiligida aynan shu muvozanat kerak.


Xulosa

Eslab qoling:

  • create<T>()((set) => ({ ...holat, ...amallar })) — store = hook
  • set((s) => ({...})) — yangilash, birlashtiriladi
  • Doim selektor: useSavat((s) => s.soni); bir nechta — useShallow
  • useSavat.getState() — komponentdan tashqarida
  • persist + AsyncStorage — avtomatik saqlash; partialize
  • Server holati — TanStack Query; global klient — Zustand; lokal — useState

Keyingi dars

34-dars: AsyncStorage — qurilmada ma'lumot saqlash