🏠 Home / Hub

🧪 Vue 13 — Composition API

← Vue Menu · ← Prev: Vite & SFC

Composition API က Vue 3 ရဲ့ core feature ပါ။ Options API (data, methods, computed ခွဲရေးတာ) ထက် logic ကို reusable composables ထဲ group လုပ်ရတာကောင်းတယ်။

1. ref() — Reactive Primitive Values

<script setup>
import { ref } from 'vue'

// ref() wraps any value in reactive container
const count   = ref(0)
const name    = ref('Mg Mg')
const loading = ref(false)
const items   = ref([])

// Access/mutate with .value in script
count.value++
name.value = 'Ko Ko'
items.value.push({ id: 1, text: 'Buy milk' })

// In template: no .value needed (auto-unwrapped)
</script>

<template>
  <p>Count: {{ count }}</p>
  <button @click="count++">+1</button>
  <input v-model="name">
</template>

2. reactive() — Reactive Objects

<script setup>
import { reactive } from 'vue'

// reactive() makes the whole object reactive (no .value needed)
const form = reactive({
  username: '',
  email: '',
  password: '',
})

const state = reactive({
  products: [],
  loading: false,
  error: null,
  page: 1,
})

// Mutate directly
function handleSubmit() {
  form.username = 'trimmed'   // reactive — template updates
  state.loading = true
}
</script>

<template>
  <input v-model="form.username">
  <input v-model="form.email">
  <p v-if="state.loading">Loading...</p>
</template>
Featureref()reactive()
Use forPrimitives (string, number, boolean) or arraysObjects / complex state
.valueRequired in scriptNot needed
DestructureWorks (ref stays reactive)Loses reactivity — use toRefs()
TemplateAuto-unwrapped (no .value)Access directly

3. computed() — Derived State

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

const todos   = ref([
  { id: 1, text: 'Learn Vue', done: true },
  { id: 2, text: 'Build app', done: false },
])
const search  = ref('')
const showAll = ref(true)

// computed = cached, only re-runs when dependencies change
const filtered = computed(() => {
  let list = showAll.value ? todos.value : todos.value.filter(t => !t.done)
  return search.value
    ? list.filter(t => t.text.toLowerCase().includes(search.value.toLowerCase()))
    : list
})

const doneCount  = computed(() => todos.value.filter(t => t.done).length)
const totalCount = computed(() => todos.value.length)
const progress   = computed(() => Math.round(doneCount.value / totalCount.value * 100))

// Writable computed (getter + setter)
const fullName = computed({
  get: () => `${firstName.value} ${lastName.value}`,
  set: (v) => {
    [firstName.value, lastName.value] = v.split(' ')
  },
})
</script>

4. watch() and watchEffect()

import { ref, watch, watchEffect } from 'vue'

const userId   = ref(1)
const userData = ref(null)

// watch: explicit source + callback
watch(userId, async (newId, oldId) => {
  console.log(`Changed from ${oldId} to ${newId}`)
  userData.value = await fetchUser(newId)
})

// watch immediate: run on mount too
watch(userId, fetchUser, { immediate: true })

// watch multiple sources
watch([userId, role], ([id, r]) => {
  console.log('User or role changed', id, r)
})

// watchEffect: auto-tracks dependencies
watchEffect(() => {
  // runs whenever userId.value changes (auto-detected)
  console.log('userId is now', userId.value)
})

// Deep watch (nested object changes)
const config = ref({ theme: 'dark', lang: 'mm' })
watch(config, (newVal) => {
  localStorage.setItem('config', JSON.stringify(newVal))
}, { deep: true })

5. Lifecycle Hooks

import { onMounted, onUnmounted, onBeforeUnmount, onUpdated } from 'vue'

// onMounted: after component is in DOM — fetch data here
onMounted(async () => {
  const data = await fetch('/api/products').then(r => r.json())
  products.value = data
})

// onUnmounted: cleanup (remove listeners, cancel timers)
let interval
onMounted(() => {
  interval = setInterval(() => { clock.value = new Date() }, 1000)
})
onUnmounted(() => clearInterval(interval))

// Lifecycle order:
// setup() → onBeforeMount → onMounted
// (re-render) → onBeforeUpdate → onUpdated
// (unmount) → onBeforeUnmount → onUnmounted

6. Composables (Reusable Logic)

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

export function useCounter(initial = 0) {
  const count = ref(initial)
  const increment = () => count.value++
  const decrement = () => count.value--
  const reset     = () => count.value = initial
  return { count, increment, decrement, reset }
}

// 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, (v) => localStorage.setItem(key, JSON.stringify(v)), { deep: true })
  return data
}

// In any component:
<script setup>
import { useCounter }      from '@/composables/useCounter'
import { useLocalStorage } from '@/composables/useLocalStorage'

const { count, increment } = useCounter(10)
const theme = useLocalStorage('theme', 'dark')
</script>

7. defineProps & defineEmits

<!-- src/components/ProductCard.vue -->
<script setup>
const props = defineProps({
  product: {
    type: Object,
    required: true,
  },
  showActions: {
    type: Boolean,
    default: true,
  },
})

const emit = defineEmits(['edit', 'delete', 'add-to-cart'])

function onEdit()   { emit('edit',        props.product) }
function onDelete() { emit('delete',      props.product.id) }
function onCart()   { emit('add-to-cart', props.product) }
</script>

<template>
  <div class="card">
    <h3>{{ product.name }}</h3>
    <p>{{ product.price }}</p>
    <div v-if="showActions">
      <button @click="onEdit">Edit</button>
      <button @click="onDelete">Delete</button>
      <button @click="onCart">Add to Cart</button>
    </div>
  </div>
</template>

<!-- Parent usage -->
<ProductCard
  :product="item"
  :show-actions="isAdmin"
  @edit="openEditModal"
  @delete="removeProduct"
  @add-to-cart="addItem"
/>

8. provide / inject (Deep Component Communication)

// Ancestor provides data
<script setup>
import { provide, ref } from 'vue'

const user  = ref({ name: 'Mg Mg', role: 'admin' })
const theme = ref('dark')

provide('user', user)    // any descendant can inject
provide('theme', theme)
provide('toggleTheme', () => {
  theme.value = theme.value === 'dark' ? 'light' : 'dark'
})
</script>

// Deep descendant (doesn't need to pass via props chain)
<script setup>
import { inject } from 'vue'

const user        = inject('user')
const theme       = inject('theme')
const toggleTheme = inject('toggleTheme')
</script>

📌 Study Checklist