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¶
// 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, keyinnuxt.configmodules'ga qo'shasan βcreatePiniaqo'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:
<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:
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):
// 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)
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
watchyoki plugin - Qatlam: Komponent β Store β API service β Backend
π― Masalalar (kamida 20 ta)¶
Asosiy (1β7)¶
useCounterStoreyarat (count,double,increment,decrement,reset). Ikki alohida komponentda ishlatib, bir xil state ko'rsatishini tasdiqla.storeToRefs(β ): yuqoridagi store'dancount,doubleni destructure qil; to'g'ridan-to'g'ri destructure bilan farqini (reaktivlik yo'qolishini) ko'rsat.useThemeStore(β ):themestate +toggleaction; har joyda joriy theme'dan foydalan.useUiStore:sidebarOpen,toggleSidebarβ header'dagi tugma va sidebar komponenti bir holatni baham ko'rsin.$patch(β ): bir nechta state'ni bitta$patchbilan yangila.- Getter parametrli (β
):
getByIdgetter β(id) => items.find(...). (Eslatma: getter funksiya qaytaradi.) - Store'lar bog'liqligi (β
):
useCartStoreuseAuthStoreni ishlatib,canCheckout(computed) ni hisoblasin.
Savat (cart) β to'liq misol (8β12)¶
- Cart store (β
β
):
items([{id,name,price,qty}]), getter'lar:totalItems,totalPrice,isEmpty. addToCart(product)(β ): mahsulot bor bo'lsaqty++, yo'q bo'lsa qo'sh.removeFromCart(id),updateQty(id, qty)(qty 0 bo'lsa o'chir),clear().- UI'ga ulang (β
β
): mahsulot ro'yxati + "Savatga" tugmasi + savat badge (
totalItems) + savat sahifasi. - Persist (β
β
): savatni
localStoragega saqla (qo'ldawatchyoki plugin); refresh'da qolsin.
Auth β to'liq (13β17)¶
- Auth store (β
β
):
user,token,isAuthenticated,login(creds),logout()(API'ni soxta qil). - Router guard (β
β
):
isAuthenticatedga qarab/dashboardni himoyala (05-modul bilan birlashtir). isAdmingetter (β ) + admin-only tugma faqat adminga ko'rinsin.- Token persist (β
β
): refresh'da login holati saqlansin; ilova ochilganda
fetchUserchaqirilsin. logout(β ): chiqishda state tozalansin va/loginga yo'naltirilsin.
Async & arxitektura (18β24)¶
- Async action (β
β
):
useProductsStoreβfetchProducts()(loading/error/data state bilan); komponent loading/error/ro'yxat holatlarini ko'rsatsin. - API qatlamini ajratish (β
β
β
): 18-da fetch'ni to'g'ridan-to'g'ri store'da yozmasdan,
services/api.js(yoki composableuseApi) ga ajrat; store o'shani chaqirsin. $subscribeplugin (β β ): Har store o'zgarishini console'ga loglaydigan global plugin yoz.- Optimistic update (β
β
β
): "Like" tugmasi β darrov UI'da
likedqil, keyin "API" xato bersa orqaga qaytar (rollback). useTenantStore(EduCore) (β β β ): Joriy tenant (currentTenant,setTenant,tenantIdgetter); barcha API so'rovlarga tenant kontekstini qo'shadigan tuzilma o'ylab top (multi-tenant frontend asosi).- Notifications store (β
β
):
notificationsmassivi,notify(msg, type)(auto-dismiss 3s),dismiss(id); global toast komponenti store'ni o'qisin. - To-Do'ni store'ga ko'chir (β
β
): Oldingi modullardagi To-Do'ni Pinia store'ga ko'chir (
todos,add/toggle/remove,activeCount/completedCountgetter, 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
β‘οΈ Keyingi: 07 β Nuxt asoslari β endi Vue ustiga "Laravel"ni qo'yamiz.