Tarkibga o'tish

04 β€” Composition API & Composables

⬅️ Oldingi: 03 β€” Komponentlar Β· 🏠 README Β· Keyingi: 05 β€” Vue Router ➑️


Bu modul Vue'ni toza arxitektura bilan yozishning kalitidir. Backend tajribang shu yerda juda asqotadi: composable β€” bu frontend'dagi "service", logikani UI'dan ajratib, qayta ishlatiladigan qilib o'raydigan birlik.

Options API vs Composition API

Eski Vue (2) β€” Options API: logika data, methods, computed, watch "qutilariga" bo'linadi. Bitta feature (masalan, "qidiruv") kodi 4 ta joyga tarqaladi.

Yangi β€” Composition API (<script setup>): logikani feature bo'yicha birga ushlaysan va composable'larga ajratasan.

Options API (feature tarqalgan):       Composition API (feature jamlangan):
data:    { search, results }            ── useSearch() ──┐
methods: { doSearch }                    search, results β”‚ hammasi
computed:{ filtered }                    doSearch         β”‚ bir joyda,
watch:   { search }                      filtered         β”‚ qayta ishlatsa bo'ladi
                                                          β”˜

Laravel analogiyasi: Options API β€” "fat controller" (hamma narsa bitta klassda, metodlarga bo'lingan). Composition API β€” logikani service va action klasslariga ajratish (DDD'dagi kabi). Composable = injektsiya qilinadigan reusable xizmat.

Quyidagi diagramma bitta "qidiruv" feature'i ikki uslubda qanday joylashishini taqqoslaydi (reaktivlik ikkalasida ham bir xil Proxy mexanizmida β€” farq faqat kodni tashkil qilishda):

Options API vs Composition API β€” xossa turi bo'yicha vs xususiyat bo'yicha tashkillash


4.1 <script setup> β€” chuqurroq

<script setup> ichida: - Top-level e'lon qilingan har narsa avtomatik template'ga ochiladi. - defineProps, defineEmits, defineModel, defineExpose β€” kompilyator makrolari (import shart emas). - Kod komponent yaratilganda bir marta ishlaydi (setup() tanasi kabi).

<script setup>
import { ref, computed, onMounted } from 'vue'

const count = ref(0)
const double = computed(() => count.value * 2)

onMounted(() => console.log('DOM tayyor'))

// hammasi avtomatik template uchun ochiq
</script>

defineExpose β€” komponent ichidan tashqariga metod ochish

<script setup> default'da hamma narsani yopiq qiladi (ota template ref orqali bola ichiga kira olmaydi). Ataylab ochish kerak bo'lsa:

<!-- Child.vue -->
<script setup>
import { ref } from 'vue'
const isOpen = ref(false)
function open() { isOpen.value = true }
defineExpose({ open })   // ota faqat shularni ko'radi
</script>
<!-- Parent -->
<script setup>
import { ref } from 'vue'
const childRef = ref(null)
</script>
<template>
  <Child ref="childRef" />
  <button @click="childRef.open()">Bolani och</button>
</template>


4.2 Lifecycle hooks (hayot tsikli)

Komponent yaratilishidan yo'q qilinishigacha bo'lgan bosqichlarga "ulanish":

<script setup>
import { onMounted, onUpdated, onUnmounted, onBeforeMount,
         onBeforeUnmount, onErrorCaptured } from 'vue'

onBeforeMount(() => {})   // DOM'ga joylashdan oldin
onMounted(() => {
  // DOM tayyor β€” DOM o'lchash, 3rd-party kutubxona init, fetch, addEventListener
})
onUpdated(() => {})       // reaktiv o'zgarish DOM'ga tushgach
onBeforeUnmount(() => {}) // o'chishdan oldin β€” tozalashga eng yaxshi joy
onUnmounted(() => {
  // listener'larni olib tashlash, interval clear, socket yopish
})
onErrorCaptured((err) => {/* bola xatosini tutib olish */ return false })
</script>

Eng ko'p ishlatiladigani β€” onMounted (DOM/fetch boshlash) va onUnmounted (tozalash).

MUHIM β€” leak'dan saqlanish:

onMounted(() => {
  const id = setInterval(tick, 1000)
  window.addEventListener('resize', onResize)

  onUnmounted(() => {              // har doim juftini tozala
    clearInterval(id)
    window.removeEventListener('resize', onResize)
  })
})

Nuxt SSR'da onMounted faqat brauzerda ishlaydi (server'da DOM yo'q). Bu β€” window/document ga murojaatni onMounted ichida qilish kerakligining sababi.

Quyidagi vaqt o'qi hooklarning chaqirilish tartibini ko'rsatadi (setup β†’ onMounted β†’ onUpdated β†’ onUnmounted), jumladan SSR/hydration'dagi xulq:

Lifecycle hooks vaqt o'qi β€” setup, onMounted, onUpdated, onUnmounted


4.3 Composable β€” qayta ishlatiladigan logika (ENG MUHIM)

Composable = reaktiv holat + logikani o'rab, qayta ishlatish uchun use...() funksiyasi. Vue'ning "custom hook"i.

Qoidalar

  1. Nomi use bilan boshlanadi: useCounter, useFetch, useAuth.
  2. Reaktiv qiymatlar (ref, computed) va funksiyalarni qaytaradi.
  3. Odatda composables/ papkada (Nuxt'da bu papka auto-import qilinadi).

Eng oddiy misol

// composables/useCounter.js
import { ref, computed } from 'vue'

export function useCounter(initial = 0, step = 1) {
  const count = ref(initial)
  const double = computed(() => count.value * 2)

  const inc = () => count.value += step
  const dec = () => count.value -= step
  const reset = () => count.value = initial

  return { count, double, inc, dec, reset }   // ref'larni qaytaramiz
}
<script setup>
import { useCounter } from '@/composables/useCounter'

const { count, double, inc, dec, reset } = useCounter(10, 2)
// destructure qilsa ham reaktivlik saqlanadi β€” chunki ular ref!
</script>

<template>
  <p>{{ count }} (x2 = {{ double }})</p>
  <button @click="inc">+</button>
  <button @click="dec">-</button>
  <button @click="reset">Reset</button>
</template>

Eslatma: composable'dan ref qaytarganing uchun destructure reaktivlikni buzmaydi (02-modulda reactive destructure muammosini ko'rgansan). Shuning uchun composable'lar odatda ref qaytaradi.

Real composable β€” useFetch (soddalashtirilgan)

// composables/useFetch.js
import { ref } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(false)

  async function execute() {
    loading.value = true
    error.value = null
    try {
      const res = await fetch(url)
      if (!res.ok) throw new Error(`HTTP ${res.status}`)
      data.value = await res.json()
    } catch (e) {
      error.value = e
    } finally {
      loading.value = false
    }
  }

  execute()
  return { data, error, loading, refetch: execute }
}
<script setup>
import { useFetch } from '@/composables/useFetch'
const { data, loading, error, refetch } = useFetch('https://api.example.com/users')
</script>

<template>
  <p v-if="loading">Yuklanmoqda...</p>
  <p v-else-if="error">Xato: {{ error.message }}</p>
  <ul v-else>
    <li v-for="u in data" :key="u.id">{{ u.name }}</li>
  </ul>
  <button @click="refetch">Qayta</button>
</template>

Bu bitta composable'ni 100 ta komponentda ishlatasan. DRY, testlanadigan, toza. (Nuxt'da useFetch allaqachon mavjud β€” 08-modul.)

Composable ichida lifecycle va cleanup

// composables/useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  function update(e) { x.value = e.clientX; y.value = e.clientY }

  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  return { x, y }
}

Composable ichida onMounted/onUnmounted ishlatish mumkin β€” ular composable'ni chaqirgan komponentga "ulanadi". Bu β€” logikani to'liq kapsulalash: listener qo'shish ham, tozalash ham composable ichida. Komponent faqat const { x, y } = useMouse() deydi.

Quyidagi diagramma bitta composable bir nechta komponentda qanday qayta ishlatilishini ko'rsatadi (har komponent o'z mustaqil reaktiv nusxasini oladi):

Composable β€” mantiqni ajratib, ko'p komponentda qayta ishlatish

Composable'larni birga ishlatish (compose qilish)

export function useUserProfile(userId) {
  const { data: user, loading } = useFetch(`/api/users/${userId}`)
  const { data: posts } = useFetch(`/api/users/${userId}/posts`)
  const isLoaded = computed(() => !!user.value && !!posts.value)
  return { user, posts, loading, isLoaded }
}

Kichik composable'lardan kattalarini quryapsan β€” xuddi service'lardan biznes-logika qatlamini qurgandek.


4.4 Composable vs boshqa yondashuvlar

Vosita Qachon
Composable Reaktiv holat + logika (stateful). State'ni baham ko'rish/qayta ishlatish
Util funksiya Sof, holati yo'q yordamchi (formatDate, slugify) β€” utils/
Komponent Vizual UI bo'lagi
Pinia store Butun ilova bo'ylab bitta global holat (auth, cart)
provide/inject Daraxtning ma'lum shoxi uchun lokal kontekst

Sezgi: Logika UI markup'siz va qayta ishlatilsa β†’ composable. Faqat bitta komponentda ishlatilsa va kichik bo'lsa β†’ komponent ichida qoldir.


4.5 Advanced reactivity (composable yozayotganda kerak bo'ladi)

import { shallowRef, triggerRef, customRef, toValue, readonly } from 'vue'

// shallowRef β€” faqat .value almashishini kuzatadi (ichini emas). Katta obyektlar uchun.
const big = shallowRef({ huge: 'data' })
big.value = { huge: 'new' }   // kuzatiladi
big.value.huge = 'x'          // kuzatilMAYDI (ichki) β€” triggerRef(big) kerak

// toValue β€” ref/getter/oddiy qiymatni "yechadi" (composable arg moslashuvchanligi uchun)
function useX(source) {       // source: ref | getter | qiymat β€” barchasini qabul qiladi
  const val = toValue(source)
}

// readonly β€” o'zgartirib bo'lmaydigan nusxa (store'dan tashqariga immutable berish)
const state = reactive({ count: 0 })
const ro = readonly(state)    // ro.count = 1 β†’ warning

toValue β€” kuchli composable yozishda muhim: foydalanuvchi useX(myRef), useX(() => x) yoki useX(5) bersa ham ishlaydi.


4.6 Composable papka strukturasi (EduCore uchun namuna)

composables/
β”œβ”€β”€ useAuth.ts          # login, logout, current user
β”œβ”€β”€ useApi.ts           # asosiy fetch wrapper (token, baseURL)
β”œβ”€β”€ usePagination.ts    # sahifalash logikasi
β”œβ”€β”€ useDebounce.ts      # debounce util-composable
β”œβ”€β”€ useToggle.ts        # boolean toggle
β”œβ”€β”€ useTenant.ts        # multi-tenant: joriy tenant konteksti
└── useTable.ts         # qidiruv+filter+sort birlashgan jadval logikasi

Bu β€” frontend'dagi "application layer". Komponentlar ingichka (thin) qoladi, logika composable'larda β€” xuddi controller'lar ingichka, logika service/action'larda bo'lgani kabi.


Xulosa

  • Composition API logikani feature bo'yicha jamlaydi (Options API tarqatadi)
  • <script setup> β€” eng qisqa, zamonaviy uslub; defineExpose bilan tanlab ochish
  • Lifecycle: onMounted (boshlash), onUnmounted (tozalash) β€” leak'dan saqlan
  • Composable = use...(), ref/computed/fn qaytaradi, qayta ishlatiladigan reaktiv logika (frontend "service")
  • Composable ichida lifecycle + cleanup β†’ to'liq kapsulalash
  • toValue, shallowRef, readonly β€” kuchli composable vositalari
  • Toza arxitektura: ingichka komponent + boy composable

🎯 Masalalar (kamida 22 ta)

Lifecycle (1–5)

  1. onMounted da console.log qil, onUnmounted da yana β€” v-if bilan komponentni o'chirib/yoqib ketma-ketlikni kuzat.
  2. Soat (β˜…): onMounted da setInterval bilan har soniya vaqtni yangila; onUnmounted da clearInterval. Tozalashni unutsang nima bo'lishini izohla.
  3. Window resize (β˜…): Oyna kengligini ekranda ko'rsat (resize listener + cleanup).
  4. Scroll position (β˜…): Sahifa scroll qiymatini kuzatib ko'rsat; tozala.
  5. onErrorCaptured (β˜…β˜…): Ataylab xato tashlaydigan bola yarat, ota uni tutib "Xatolik yuz berdi" ko'rsatsin.

Asosiy composable'lar (6–13)

  1. useToggle(initial=false) β†’ { value, toggle, setTrue, setFalse }. Modal/sidebar'da ishlat.
  2. useCounter(initial, step) β†’ { count, inc, dec, reset, double } (yuqoridagini o'zing qayta yoz).
  3. useLocalStorage(key, default) (β˜…): localStorage bilan sinxron reaktiv ref (watch ichida saqla, init'da o'qi).
  4. useDebounce(value, delay) (β˜…): ref'ni debounce qilingan reaktiv qiymatga aylantir.
  5. useMouse() β†’ { x, y } (listener + cleanup composable ichida).
  6. useWindowSize() (β˜…) β†’ { width, height }, reaktiv.
  7. useClipboard() (β˜…): { copy, copied } β€” matn nusxalash, 2s "copied" holati.
  8. useInterval(callback, ms) (β˜…): start/stop boshqaruvi bilan.

Data composable'lari (14–18)

  1. useFetch(url) (β˜…): yuqoridagini qayta yoz; loading/error/data/refetch bilan.
  2. usePagination(items, perPage) (β˜…β˜…): currentPage, totalPages, paginatedItems, next/prev/goTo.
  3. useSearch(items, keys) (β˜…β˜…): qidiruv matni bo'yicha keys lar ichidan filtrlangan ro'yxat qaytarsin.
  4. useSort(items) (β˜…β˜…): ustun bo'yicha asc/desc saralash, holat boshqaruvi bilan.
  5. useTable (β˜…β˜…β˜…): 15+16+17 ni birlashtir β€” bitta composable qidiruv+filter+sort+pagination beradigan. EduCore jadvallari uchun asos.

Arxitektura va kompozitsiya (19–24)

  1. Compose qilish (β˜…β˜…): useUserProfile(id) ni useFetch ustiga qur (user + posts birga).
  2. toValue (β˜…β˜…): useDoubled(source) yoz β€” source ref, getter yoki oddiy son bo'lsa ham ishlasin (toValue).
  3. defineExpose (β˜…β˜…): VideoPlayer komponenti play()/pause() metodlarini defineExpose qilsin; ota tugma orqali boshqarsin.
  4. Composable + komponent (β˜…β˜…): useFetch + DataList (03-modul scoped slot) ni birlashtirib, har qanday API ro'yxatini ko'rsatadigan reusable blok yarat.
  5. Refactor (β˜…β˜…β˜…): Quyidagi "fat" komponentni composable(lar)ga ajrat: > Bitta komponentda: qidiruv inputi + API'dan ro'yxat olish + filterlash + pagination + localStorage'ga oxirgi qidiruvni saqlash β€” hammasi aralashgan. Buni useSearch, useFetch, usePagination, useLocalStorage ga bo'lib, komponentni ingichka qil.
  6. EduCore useAuth skeleti (β˜…β˜…β˜…): user (ref), isAuthenticated (computed), login(creds), logout(), fetchUser() β€” token'ni useLocalStorage bilan sinxronla (hozircha API'ni soxta qil).

βœ… Tanlangan yechimlar

6 β€” useToggle
// composables/useToggle.js
import { ref } from 'vue'
export function useToggle(initial = false) {
  const value = ref(initial)
  const toggle = () => value.value = !value.value
  const setTrue = () => value.value = true
  const setFalse = () => value.value = false
  return { value, toggle, setTrue, setFalse }
}
8 β€” useLocalStorage
// composables/useLocalStorage.js
import { ref, watch } from 'vue'
export function useLocalStorage(key, defaultValue) {
  const stored = localStorage.getItem(key)
  const data = ref(stored ? JSON.parse(stored) : defaultValue)

  watch(data, (val) => {
    localStorage.setItem(key, JSON.stringify(val))
  }, { deep: true })

  return data
}
// Ishlatish: const theme = useLocalStorage('theme', 'dark')
15 β€” usePagination
// composables/usePagination.js
import { ref, computed, toValue } from 'vue'
export function usePagination(items, perPage = 10) {
  const currentPage = ref(1)
  const list = computed(() => toValue(items))   // ref yoki oddiy massiv bo'lsa ham

  const totalPages = computed(() =>
    Math.max(1, Math.ceil(list.value.length / perPage))
  )
  const paginatedItems = computed(() => {
    const start = (currentPage.value - 1) * perPage
    return list.value.slice(start, start + perPage)
  })

  const next = () => { if (currentPage.value < totalPages.value) currentPage.value++ }
  const prev = () => { if (currentPage.value > 1) currentPage.value-- }
  const goTo = (p) => { currentPage.value = Math.min(Math.max(1, p), totalPages.value) }

  return { currentPage, totalPages, paginatedItems, next, prev, goTo }
}
18 β€” useTable (qisqartirilgan tuzilma)
// composables/useTable.js
import { ref, computed } from 'vue'
export function useTable(source, { searchKeys = [], perPage = 10 } = {}) {
  const search = ref('')
  const sortKey = ref(null)
  const sortDir = ref('asc')
  const page = ref(1)

  const searched = computed(() => {
    if (!search.value) return source.value
    const q = search.value.toLowerCase()
    return source.value.filter(row =>
      searchKeys.some(k => String(row[k]).toLowerCase().includes(q))
    )
  })
  const sorted = computed(() => {
    if (!sortKey.value) return searched.value
    return [...searched.value].sort((a, b) => {
      const r = a[sortKey.value] > b[sortKey.value] ? 1 : -1
      return sortDir.value === 'asc' ? r : -r
    })
  })
  const totalPages = computed(() => Math.max(1, Math.ceil(sorted.value.length / perPage)))
  const rows = computed(() => {
    const start = (page.value - 1) * perPage
    return sorted.value.slice(start, start + perPage)
  })
  function setSort(key) {
    if (sortKey.value === key) sortDir.value = sortDir.value === 'asc' ? 'desc' : 'asc'
    else { sortKey.value = key; sortDir.value = 'asc' }
    page.value = 1
  }
  return { search, sortKey, sortDir, page, rows, totalPages, setSort }
}

➑️ Keyingi: 05 β€” Vue Router