Chuyển đến nội dung chính

Bài 2: Reactivity System & Composition API

ref(), reactive(), computed(), watch(), watchEffect(). Composition API vs Options API. Reactivity fundamentals, Proxy-based reactivity. TypeScript integration, defineProps, defineEmits.

💻 Lập trình — Bài 2 Bài 2: Reactivity System & Composition API

Vue.js & Nuxt: Từ Cơ bản đến Nâng cao

Phần 1: Vue.js Fundamentals

xdev.asia

1. ref() — Reactive Primitive

<script setup lang="ts">
import { ref } from 'vue'

const count = ref(0)          // Ref<number>
const name = ref('Vue')       // Ref<string>
const isVisible = ref(true)   // Ref<boolean>
const user = ref<User | null>(null) // Ref<User | null>

// Truy cập qua .value trong script
console.log(count.value) // 0
count.value++

// Trong template — auto unwrap (không cần .value)
</script>

<template>
  <p>{{ count }}</p>
  <p>{{ name }}</p>
</template>

2. reactive() — Reactive Object

<script setup lang="ts">
import { reactive } from 'vue'

interface FormState {
  name: string
  email: string
  age: number
}

const form = reactive<FormState>({
  name: '',
  email: '',
  age: 0,
})

// Truy cập trực tiếp (không cần .value)
form.name = 'Duy'
form.age = 25

// ⚠️ Không reassign — sẽ mất reactivity
// form = { name: 'New', email: '', age: 0 } // ❌
</script>

3. computed() — Derived State

<script setup lang="ts">
import { ref, computed } from 'vue'

const firstName = ref('Duy')
const lastName = ref('Tran')

// Read-only computed
const fullName = computed(() => `${firstName.value} ${lastName.value}`)

// Writable computed
const fullNameWritable = computed({
  get: () => `${firstName.value} ${lastName.value}`,
  set: (value: string) => {
    const [first, ...rest] = value.split(' ')
    firstName.value = first
    lastName.value = rest.join(' ')
  },
})

// Computed với TypeScript
const items = ref<{ price: number; qty: number }[]>([])
const total = computed(() =>
  items.value.reduce((sum, item) => sum + item.price * item.qty, 0)
)
</script>

4. watch() & watchEffect()

<script setup lang="ts">
import { ref, watch, watchEffect } from 'vue'

const searchQuery = ref('')
const page = ref(1)

// Watch single ref
watch(searchQuery, (newVal, oldVal) => {
  console.log(`Search: ${oldVal} → ${newVal}`)
  page.value = 1 // Reset page khi search thay đổi
})

// Watch multiple sources
watch([searchQuery, page], ([newQuery, newPage], [oldQuery, oldPage]) => {
  fetchResults(newQuery, newPage)
})

// Watch with options
watch(
  () => form.name,
  (newName) => { validateName(newName) },
  { immediate: true, deep: false }
)

// watchEffect — auto-track dependencies
watchEffect(() => {
  // Tự động track searchQuery.value và page.value
  console.log(`Fetching: ${searchQuery.value}, page ${page.value}`)
})

// Cleanup
watchEffect((onCleanup) => {
  const controller = new AbortController()
  fetch(`/api/search?q=${searchQuery.value}`, { signal: controller.signal })
  onCleanup(() => controller.abort())
})
</script>

5. Composition API vs Options API

<!-- ❌ Options API (legacy) -->
<script lang="ts">
export default {
  data() {
    return { count: 0 }
  },
  computed: {
    doubled() { return this.count * 2 }
  },
  methods: {
    increment() { this.count++ }
  },
}
</script>

<!-- ✅ Composition API (recommended) -->
<script setup lang="ts">
import { ref, computed } from 'vue'

const count = ref(0)
const doubled = computed(() => count.value * 2)
function increment() { count.value++ }
</script>

6. TypeScript Integration

<script setup lang="ts">
// Props với TypeScript
const props = defineProps<{
  title: string
  count?: number
  items: string[]
}>()

// Props với defaults
const props = withDefaults(defineProps<{
  title: string
  count?: number
}>(), {
  count: 0,
})

// Emits với TypeScript
const emit = defineEmits<{
  (e: 'update', value: string): void
  (e: 'delete', id: number): void
}>()

emit('update', 'new value')
emit('delete', 123)
</script>

Bài tiếp theo: Components, Props & Events — slots, provide/inject, lifecycle hooks.