Tarkibga o'tish

06 β€” Pinia (State Management)

⬅️ Oldingi: 05 β€” Vue Router Β· 🏠 README Β· Keyingi: 07 β€” Nuxt asoslari ➑️


ref/composable lokal holat uchun yetadi. Lekin butun ilova bo'ylab baham ko'riladigan holat (joriy foydalanuvchi, savat, til, EduCore'da joriy tenant) kerak bo'lsa β€” Pinia.

Pinia β€” Vue'ning rasmiy state-management kutubxonasi (Vuex'ning vorisi). Soddaroq, TypeScript-do'st, DevTools bilan zo'r.

Laravel analogiyasi: Pinia store β€” singleton service. Bir marta yaratiladi, butun ilova bir xil instansiyani ishlatadi. State β€” service property'lari, getters β€” accessor'lar, actions β€” service metodlari (biznes-logika shu yerda).

Qachon Pinia, qachon yo'q?

Holat Yechim
Bitta komponentga tegishli lokal ref
Bir necha qo'shni komponent props/emit yoki composable
Daraxtning ma'lum shoxi provide/inject
Butun ilova, ko'p joydan o'qiladi/yoziladi Pinia

Hamma narsani store'ga tiqishtirma. Pinia β€” global holat uchun. Lokal narsa lokal qolsin.


6.1 O'rnatish

npm install pinia
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

createApp(App).use(createPinia()).mount('#app')

Nuxt'da: npm i @pinia/nuxt, keyin nuxt.config modules'ga qo'shasan β€” createPinia qo'lda kerak emas (07-modul).


6.2 Store yaratish β€” Setup syntax (tavsiya)

Composition API uslubi β€” eng moslashuvchan, <script setup> ga o'xshaydi:

// stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCounterStore = defineStore('counter', () => {
  // STATE β†’ ref
  const count = ref(0)
  const history = ref([])

  // GETTERS β†’ computed
  const double = computed(() => count.value * 2)
  const isEven = computed(() => count.value % 2 === 0)

  // ACTIONS β†’ funksiyalar (sync yoki async)
  function increment() {
    count.value++
    history.value.push(count.value)
  }
  async function fetchInitial() {
    const res = await fetch('/api/counter')
    count.value = (await res.json()).value
  }

  return { count, history, double, isEven, increment, fetchInitial }
})

Moslik: - ref = state - computed = getter - function = action

Quyidagi diagramma store'ning uch qismi (state / getters / actions) o'zaro qanday bog'lanishini ko'rsatadi:

Pinia store anatomiyasi: state, getters, actions

<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
</script>

<template>
  <p>{{ counter.count }} (x2 = {{ counter.double }})</p>
  <button @click="counter.increment()">+</button>
</template>

Option syntax (Vuex'ga o'xshash, alternativa)

export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  getters: {
    double: (state) => state.count * 2,
  },
  actions: {
    increment() { this.count++ },          // this β€” store
    async fetchInitial() { /* await ... */ },
  },
})

Ikkalasi ham ishlaydi. Setup syntax zamonaviyroq va composable'lar bilan yaxshiroq birikadi. Bu qo'llanmada uni ishlatamiz.


6.3 Store'dan foydalanish β€” destructure tuzog'i

<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'

const counter = useCounterStore()

// ❌ XATO β€” reaktivlikni uzadi
const { count, double } = counter

// βœ… State/getters uchun storeToRefs
const { count, double } = storeToRefs(counter)

// βœ… Actions'ni to'g'ridan-to'g'ri destructure qilsa bo'ladi (ular funksiya, reaktiv emas)
const { increment } = counter
</script>

Nega? Store β€” reactive obyekt (02-modul!). Uni to'g'ridan-to'g'ri destructure qilsang, reaktivlik uziladi. storeToRefs state va getter'larni reaktiv ref'larga o'raydi. Actions esa shunchaki funksiya β€” to'g'ridan-to'g'ri olsa bo'ladi.

Quyidagi diagramma bitta store'ni ko'p komponent qanday baham ko'rishini ko'rsatadi: biri action chaqirib state'ni o'zgartiradi, qolganlari esa Proxy reaktivligi tufayli avtomatik yangilanadi:

Komponent va store: action chaqirish va reaktiv yangilanish


6.4 State'ni o'zgartirish usullari

const store = useCounterStore()

// 1) action orqali (TAVSIYA β€” logika bir joyda, DevTools'da kuzatiladi)
store.increment()

// 2) to'g'ridan-to'g'ri (mumkin, lekin oddiy holatlar uchun)
store.count++

// 3) $patch β€” bir nechta o'zgarishni birga (bitta yangilanish)
store.$patch({ count: 10, name: 'x' })
store.$patch((state) => {              // murakkab (massivga push va h.k.)
  state.items.push(newItem)
  state.count++
})

// 4) $reset β€” boshlang'ich holatga (faqat Option syntax'da avtomatik;
//    Setup syntax'da o'zing reset action yozasan)

Vuex'dan farqli: Pinia'da mutations yo'q. To'g'ridan-to'g'ri o'zgartirish yoki action. Bu β€” kamroq boilerplate.


6.5 Store'lar bir-birini ishlatishi

// stores/cart.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { useAuthStore } from './auth'

export const useCartStore = defineStore('cart', () => {
  const auth = useAuthStore()          // boshqa store'ni chaqir
  const items = ref([])

  const canCheckout = computed(() =>
    auth.isAuthenticated && items.value.length > 0
  )
  return { items, canCheckout }
})

Store'lar bir-birini bemalol ishlatadi — service'lar bir-birini DI orqali chaqirgani kabi. Faqat aylanma bog'liqlik (A→B→A)dan ehtiyot bo'l.


6.6 Real store β€” useAuthStore (EduCore namuna)

// stores/auth.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useAuthStore = defineStore('auth', () => {
  const user = ref(null)
  const token = ref(localStorage.getItem('token') || null)

  const isAuthenticated = computed(() => !!token.value)
  const isAdmin = computed(() => user.value?.role === 'admin')

  async function login(credentials) {
    const res = await fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(credentials),
    })
    if (!res.ok) throw new Error('Login xato')
    const data = await res.json()
    token.value = data.token
    user.value = data.user
    localStorage.setItem('token', data.token)
  }

  async function fetchUser() {
    if (!token.value) return
    const res = await fetch('/api/me', {
      headers: { Authorization: `Bearer ${token.value}` },
    })
    user.value = await res.json()
  }

  function logout() {
    token.value = null
    user.value = null
    localStorage.removeItem('token')
  }

  return { user, token, isAuthenticated, isAdmin, login, fetchUser, logout }
})

Router guard bilan birlashtirish (05-modul):

router.beforeEach((to) => {
  const auth = useAuthStore()
  if (to.meta.requiresAuth && !auth.isAuthenticated) {
    return { name: 'login' }
  }
})


6.7 Persist (saqlash) va pluginlar

State'ni refresh'da yo'qotmaslik uchun localStorage ga saqlash:

Qo'lda (oddiy):

import { watch } from 'vue'
// store ichida:
watch(token, (val) => {
  val ? localStorage.setItem('token', val) : localStorage.removeItem('token')
})

Plugin bilan (avtomatik, ko'p store uchun):

npm install pinia-plugin-persistedstate
// main.js
import piniaPersist from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPersist)
// store (Option syntax'da):
export const useAuthStore = defineStore('auth', {
  state: () => ({ token: null }),
  persist: true,   // butun store localStorage'ga
})

O'z plugining (DI/logging uchun)

pinia.use(({ store }) => {
  // har store yaratilganda ishlaydi
  store.$subscribe((mutation, state) => {
    console.log(`[${store.$id}] o'zgardi`, state)   // global logging
  })
})

$subscribe β€” store o'zgarishini global tinglash. Logging, analytics, sync uchun qulay.


6.8 Arxitektura: store nima qiladi, nima qilmaydi

Store ichiga: - Global state (auth, ui sozlamalari, savat, tenant) - O'sha state'ni o'zgartiruvchi biznes-logika (action) - Hosilaviy qiymatlar (getter)

Store ichiga EMAS: - Vizual/UI logika (komponentda qolsin) - Faqat bitta komponentga kerakli vaqtinchalik holat (lokal ref) - Og'ir API qatlami β€” uni alohida services/api.js ga ajratib, store action'i undan foydalansin (DDD'dagi repository/service ajratimiga o'xshash)

Komponent  β†’  Store (action)  β†’  API service  β†’  Backend
   (UI)        (state+biznes)      (HTTP)

Bu qatlamlash β€” backend'dagi controller β†’ service β†’ repository ga to'g'ridan-to'g'ri mos keladi. Sen buni allaqachon bilasan.


Xulosa

  • Pinia β€” global holat (singleton service kabi); lokal narsani store'ga tiqma
  • Setup syntax: ref=state, computed=getter, function=action
  • storeToRefs β€” state/getter destructure uchun (actions'ni to'g'ridan-to'g'ri ol)
  • O'zgartirish: action (afzal), to'g'ridan-to'g'ri, $patch. Mutations yo'q
  • Store'lar bir-birini ishlatadi (DI kabi)
  • Persist β€” qo'lda watch yoki plugin
  • Qatlam: Komponent β†’ Store β†’ API service β†’ Backend

🎯 Masalalar (kamida 20 ta)

Asosiy (1–7)

  1. useCounterStore yarat (count, double, increment, decrement, reset). Ikki alohida komponentda ishlatib, bir xil state ko'rsatishini tasdiqla.
  2. storeToRefs (β˜…): yuqoridagi store'dan count, double ni destructure qil; to'g'ridan-to'g'ri destructure bilan farqini (reaktivlik yo'qolishini) ko'rsat.
  3. useThemeStore (β˜…): theme state + toggle action; har joyda joriy theme'dan foydalan.
  4. useUiStore: sidebarOpen, toggleSidebar β€” header'dagi tugma va sidebar komponenti bir holatni baham ko'rsin.
  5. $patch (β˜…): bir nechta state'ni bitta $patch bilan yangila.
  6. Getter parametrli (β˜…): getById getter β€” (id) => items.find(...). (Eslatma: getter funksiya qaytaradi.)
  7. Store'lar bog'liqligi (β˜…): useCartStore useAuthStore ni ishlatib, canCheckout (computed) ni hisoblasin.

Savat (cart) β€” to'liq misol (8–12)

  1. Cart store (β˜…β˜…): items ([{id,name,price,qty}]), getter'lar: totalItems, totalPrice, isEmpty.
  2. addToCart(product) (β˜…): mahsulot bor bo'lsa qty++, yo'q bo'lsa qo'sh.
  3. removeFromCart(id), updateQty(id, qty) (qty 0 bo'lsa o'chir), clear().
  4. UI'ga ulang (β˜…β˜…): mahsulot ro'yxati + "Savatga" tugmasi + savat badge (totalItems) + savat sahifasi.
  5. Persist (β˜…β˜…): savatni localStorage ga saqla (qo'lda watch yoki plugin); refresh'da qolsin.

Auth β€” to'liq (13–17)

  1. Auth store (β˜…β˜…): user, token, isAuthenticated, login(creds), logout() (API'ni soxta qil).
  2. Router guard (β˜…β˜…): isAuthenticated ga qarab /dashboard ni himoyala (05-modul bilan birlashtir).
  3. isAdmin getter (β˜…) + admin-only tugma faqat adminga ko'rinsin.
  4. Token persist (β˜…β˜…): refresh'da login holati saqlansin; ilova ochilganda fetchUser chaqirilsin.
  5. logout (β˜…): chiqishda state tozalansin va /login ga yo'naltirilsin.

Async & arxitektura (18–24)

  1. Async action (β˜…β˜…): useProductsStore β€” fetchProducts() (loading/error/data state bilan); komponent loading/error/ro'yxat holatlarini ko'rsatsin.
  2. API qatlamini ajratish (β˜…β˜…β˜…): 18-da fetch'ni to'g'ridan-to'g'ri store'da yozmasdan, services/api.js (yoki composable useApi) ga ajrat; store o'shani chaqirsin.
  3. $subscribe plugin (β˜…β˜…): Har store o'zgarishini console'ga loglaydigan global plugin yoz.
  4. Optimistic update (β˜…β˜…β˜…): "Like" tugmasi β€” darrov UI'da liked qil, keyin "API" xato bersa orqaga qaytar (rollback).
  5. useTenantStore (EduCore) (β˜…β˜…β˜…): Joriy tenant (currentTenant, setTenant, tenantId getter); barcha API so'rovlarga tenant kontekstini qo'shadigan tuzilma o'ylab top (multi-tenant frontend asosi).
  6. Notifications store (β˜…β˜…): notifications massivi, notify(msg, type) (auto-dismiss 3s), dismiss(id); global toast komponenti store'ni o'qisin.
  7. To-Do'ni store'ga ko'chir (β˜…β˜…): Oldingi modullardagi To-Do'ni Pinia store'ga ko'chir (todos, add/toggle/remove, activeCount/completedCount getter, persist). Komponent endi ingichka bo'lsin.

βœ… Tanlangan yechimlar

8–10 β€” Cart store
// stores/cart.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCartStore = defineStore('cart', () => {
  const items = ref([])

  const totalItems = computed(() =>
    items.value.reduce((s, i) => s + i.qty, 0)
  )
  const totalPrice = computed(() =>
    items.value.reduce((s, i) => s + i.price * i.qty, 0)
  )
  const isEmpty = computed(() => items.value.length === 0)

  function addToCart(product) {
    const existing = items.value.find(i => i.id === product.id)
    if (existing) existing.qty++
    else items.value.push({ ...product, qty: 1 })
  }
  function updateQty(id, qty) {
    const item = items.value.find(i => i.id === id)
    if (!item) return
    if (qty <= 0) removeFromCart(id)
    else item.qty = qty
  }
  function removeFromCart(id) {
    items.value = items.value.filter(i => i.id !== id)
  }
  function clear() { items.value = [] }

  return { items, totalItems, totalPrice, isEmpty,
           addToCart, updateQty, removeFromCart, clear }
})
21 β€” Optimistic update (rollback)
// stores/posts.js (action ichida)
async function toggleLike(post) {
  const prev = post.liked
  post.liked = !post.liked          // 1) darrov UI yangilanadi
  post.likes += post.liked ? 1 : -1
  try {
    await fakeApiToggleLike(post.id) // 2) serverga
  } catch (e) {
    post.liked = prev                // 3) xato bo'lsa orqaga
    post.likes += post.liked ? 1 : -1
    throw e
  }
}
20 β€” Global logging plugin
// main.js
const pinia = createPinia()
pinia.use(({ store }) => {
  store.$subscribe((mutation, state) => {
    console.log(`[${store.$id}]`, mutation.type, state)
  })
})

➑️ Keyingi: 07 β€” Nuxt asoslari β€” endi Vue ustiga "Laravel"ni qo'yamiz.