Vue 3 Composition API: Hướng Dẫn Đầy Đủ Để Làm Chủ Reactivity
Làm chủ Vue 3 Composition API qua hướng dẫn thực hành này. Tìm hiểu ref, reactive, computed, watch và composables để xây dựng ứng dụng Vue hiệu suất cao.

Vue 3 Composition API là một bước tiến lớn trong cách cấu trúc các component Vue. Cách tiếp cận này tổ chức mã nguồn theo tính năng thay vì theo tùy chọn, giúp việc tái sử dụng logic và bảo trì các ứng dụng phức tạp trở nên dễ dàng hơn.
Hướng dẫn này giả định bạn đã có kiến thức cơ bản về Vue.js. Các ví dụ sử dụng cú pháp <script setup> được giới thiệu trong Vue 3.2, hiện là cách tiếp cận được khuyến nghị.
Tìm hiểu Reactivity với ref và reactive
Reactivity là cốt lõi của Vue 3. Hai primitive chính để tạo dữ liệu reactive là: ref cho các giá trị nguyên thủy và reactive cho các đối tượng phức tạp.
Hàm ref tạo một tham chiếu reactive bao bọc một giá trị. Để truy cập hoặc thay đổi giá trị này trong script, thuộc tính .value phải được sử dụng. Trong template, Vue tự động unwrap thuộc tính này.
<script setup>
import { ref } from 'vue'
// Create a ref with initial value 0
const count = ref(0)
// Function that increments the counter
// Note: .value is required in the script
const increment = () => {
count.value++
}
// Reset function
const reset = () => {
count.value = 0
}
</script>
<template>
<!-- In the template, no .value needed -->
<div class="counter">
<p>Counter: {{ count }}</p>
<button @click="increment">+1</button>
<button @click="reset">Reset</button>
</div>
</template>Đối với các đối tượng có nhiều thuộc tính liên quan, reactive cung cấp cú pháp tự nhiên hơn mà không cần .value.
<script setup>
import { reactive } from 'vue'
// reactive for complex objects
// All properties are automatically reactive
const user = reactive({
name: 'Jane Smith',
email: 'jane@example.com',
preferences: {
theme: 'dark',
notifications: true
}
})
// Direct property modification (no .value)
const updateTheme = (newTheme) => {
user.preferences.theme = newTheme
}
// Warning: do not reassign the entire object
// user = { ... } would break reactivity
</script>
<template>
<div>
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
<p>Theme: {{ user.preferences.theme }}</p>
</div>
</template>Quy tắc chung: sử dụng ref cho các giá trị nguyên thủy (string, number, boolean) và reactive cho các đối tượng có cấu trúc.
Thuộc tính Computed với computed
Thuộc tính computed suy ra giá trị từ state reactive. Chúng được lưu vào cache và chỉ tính toán lại khi các dependency thay đổi, mang lại hiệu suất rất cao.
<script setup>
import { ref, computed } from 'vue'
// Product list
const products = ref([
{ id: 1, name: 'Laptop', price: 999, inStock: true },
{ id: 2, name: 'Mouse', price: 29, inStock: true },
{ id: 3, name: 'Keyboard', price: 79, inStock: false },
{ id: 4, name: 'Monitor', price: 299, inStock: true }
])
// Active filter
const showOnlyInStock = ref(false)
// computed: automatic filtering based on toggle
// Only recalculates if products or showOnlyInStock changes
const filteredProducts = computed(() => {
if (showOnlyInStock.value) {
return products.value.filter(p => p.inStock)
}
return products.value
})
// computed: total calculation with caching
const totalValue = computed(() => {
return filteredProducts.value.reduce((sum, p) => sum + p.price, 0)
})
// computed: number of displayed items
const productCount = computed(() => filteredProducts.value.length)
</script>
<template>
<div>
<label>
<input type="checkbox" v-model="showOnlyInStock" />
Show only in-stock products
</label>
<p>{{ productCount }} products - Total: ${{ totalValue }}</p>
<ul>
<li v-for="product in filteredProducts" :key="product.id">
{{ product.name }} - ${{ product.price }}
</li>
</ul>
</div>
</template>Thuộc tính computed cũng có thể ghi được (writable) với getter và setter, hữu ích cho các phép biến đổi hai chiều.
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
// computed with getter AND setter
const fullName = computed({
// Read: combine first and last name
get() {
return `${firstName.value} ${lastName.value}`
},
// Write: split the string into first/last name
set(newValue) {
const parts = newValue.split(' ')
firstName.value = parts[0] || ''
lastName.value = parts.slice(1).join(' ') || ''
}
})
</script>
<template>
<!-- Modifying fullName updates firstName and lastName -->
<input v-model="fullName" placeholder="Full name" />
<p>First name: {{ firstName }}</p>
<p>Last name: {{ lastName }}</p>
</template>Watcher với watch và watchEffect
Watcher thực thi các tác dụng phụ để phản hồi lại các thay đổi dữ liệu. Vue 3 cung cấp hai cách tiếp cận: watch để kiểm soát chính xác và watchEffect để theo dõi tự động.
watch cung cấp khả năng kiểm soát chính xác các dependency và cung cấp cả giá trị cũ lẫn giá trị mới. watchEffect đơn giản hơn khi tất cả các dependency reactive được sử dụng đều cần kích hoạt hiệu ứng.
<script setup>
import { ref, watch, watchEffect } from 'vue'
const searchQuery = ref('')
const searchResults = ref([])
const isLoading = ref(false)
// watch: observes a specific source
// Provides oldValue and newValue, allows debouncing
watch(searchQuery, async (newQuery, oldQuery) => {
console.log(`Search changed from "${oldQuery}" to "${newQuery}"`)
// Don't search if less than 3 characters
if (newQuery.length < 3) {
searchResults.value = []
return
}
isLoading.value = true
try {
// Simulated API call
const response = await fetch(`/api/search?q=${newQuery}`)
searchResults.value = await response.json()
} finally {
isLoading.value = false
}
}, {
// Watcher options
debounce: 300, // Wait 300ms after last keystroke
immediate: false // Don't execute immediately
})
// watchEffect: automatic dependency tracking
// Runs immediately and on every change
watchEffect(() => {
// All refs used here are automatically tracked
console.log(`Current state: ${searchResults.value.length} results`)
console.log(`Loading: ${isLoading.value}`)
})
</script>
<template>
<div>
<input v-model="searchQuery" placeholder="Search..." />
<p v-if="isLoading">Loading...</p>
<ul v-else>
<li v-for="result in searchResults" :key="result.id">
{{ result.title }}
</li>
</ul>
</div>
</template>Để theo dõi các đối tượng hoặc mảng lồng nhau, tùy chọn deep là cần thiết với watch.
<script setup>
import { reactive, watch } from 'vue'
const settings = reactive({
display: {
theme: 'light',
fontSize: 14
},
notifications: {
email: true,
push: false
}
})
// deep: true to observe nested changes
watch(
() => settings.display,
(newDisplay) => {
console.log('Display settings changed:', newDisplay)
// Save to localStorage for example
localStorage.setItem('display', JSON.stringify(newDisplay))
},
{ deep: true }
)
</script>Sẵn sàng chinh phục phỏng vấn Vue.js / Nuxt.js?
Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.
Tạo Composable tái sử dụng
Composable là các hàm đóng gói logic reactive có thể tái sử dụng. Đây là một trong những điểm mạnh lớn nhất của Composition API để chia sẻ mã giữa các component.
import { ref, watchEffect, toValue } from 'vue'
// Composable for HTTP requests
// Automatically handles loading, errors, and refetching
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const isLoading = ref(false)
// Reusable fetch function
async function fetchData() {
isLoading.value = true
error.value = null
try {
// toValue() allows accepting a ref or a value
const response = await fetch(toValue(url))
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
data.value = await response.json()
} catch (e) {
error.value = e.message
} finally {
isLoading.value = false
}
}
// watchEffect for automatic refetch if url is a ref
watchEffect(() => {
fetchData()
})
// Expose state and methods
return {
data,
error,
isLoading,
refetch: fetchData
}
}Composable này sau đó có thể được sử dụng theo cách khai báo trong bất kỳ component nào.
<script setup>
import { useFetch } from '@/composables/useFetch'
// Simple composable usage
const { data: users, isLoading, error, refetch } = useFetch('/api/users')
</script>
<template>
<div>
<button @click="refetch" :disabled="isLoading">
Refresh
</button>
<p v-if="isLoading">Loading users...</p>
<p v-else-if="error" class="error">Error: {{ error }}</p>
<ul v-else-if="users">
<li v-for="user in users" :key="user.id">
{{ user.name }}
</li>
</ul>
</div>
</template>Dưới đây là một ví dụ composable khác để quản lý trạng thái form kèm theo validation.
import { reactive, computed } from 'vue'
// Form management composable
export function useForm(initialValues, validationRules) {
// Form state
const form = reactive({
values: { ...initialValues },
errors: {},
touched: {}
})
// Validate a specific field
const validateField = (field) => {
const rules = validationRules[field]
if (!rules) return true
for (const rule of rules) {
const result = rule(form.values[field])
if (result !== true) {
form.errors[field] = result
return false
}
}
form.errors[field] = null
return true
}
// Validate entire form
const validate = () => {
let isValid = true
for (const field in validationRules) {
if (!validateField(field)) {
isValid = false
}
}
return isValid
}
// computed: form is valid if no errors
const isValid = computed(() => {
return Object.values(form.errors).every(e => !e)
})
// Mark a field as touched (to display errors)
const touch = (field) => {
form.touched[field] = true
validateField(field)
}
// Reset the form
const reset = () => {
form.values = { ...initialValues }
form.errors = {}
form.touched = {}
}
return {
form,
isValid,
validate,
validateField,
touch,
reset
}
}Quản lý Lifecycle Hook
Composition API cung cấp các lifecycle hook dưới dạng hàm được gọi bên trong setup. Các hook này thực thi mã tại những thời điểm cụ thể trong vòng đời của component.
Luôn dọn dẹp các tác dụng phụ (timer, event listener, subscription) trong onUnmounted để tránh rò rỉ bộ nhớ.
<script setup>
import {
ref,
onMounted,
onUnmounted,
onBeforeUpdate,
onUpdated
} from 'vue'
const windowWidth = ref(window.innerWidth)
const updateCount = ref(0)
// Handler for resize
const handleResize = () => {
windowWidth.value = window.innerWidth
}
// onMounted: component inserted into DOM
// Ideal for initial API calls and event listeners
onMounted(() => {
console.log('Component mounted')
window.addEventListener('resize', handleResize)
// Example: initialize a third-party library
// chart = new Chart(chartRef.value, config)
})
// onUnmounted: component removed from DOM
// CRITICAL: clean up all side effects
onUnmounted(() => {
console.log('Component unmounted')
window.removeEventListener('resize', handleResize)
// Clean up subscriptions, timers, etc.
// chart?.destroy()
})
// onBeforeUpdate: before DOM update
onBeforeUpdate(() => {
console.log('Before update')
})
// onUpdated: after DOM update
onUpdated(() => {
updateCount.value++
console.log(`DOM updated (${updateCount.value} times)`)
})
</script>
<template>
<div>
<p>Window width: {{ windowWidth }}px</p>
<p>DOM updates: {{ updateCount }}</p>
</div>
</template>Template Ref và truy cập DOM
Template ref cung cấp quyền truy cập trực tiếp đến các phần tử DOM hoặc instance của component con. Tính năng này vẫn hữu ích cho các trường hợp cần thao tác trực tiếp.
<script setup>
import { ref, onMounted } from 'vue'
// ref for the DOM element
// Same name as the ref attribute in the template
const inputRef = ref(null)
// Function to focus the input
const focusInput = () => {
// Access to the native DOM element
inputRef.value?.focus()
}
// Auto-focus on mount
onMounted(() => {
focusInput()
})
// Expose the method to parent if needed
defineExpose({
focus: focusInput
})
</script>
<template>
<div>
<!-- The ref attribute binds the element to inputRef -->
<input ref="inputRef" type="text" placeholder="Auto-focused" />
<button @click="focusInput">Refocus</button>
</div>
</template>Giao tiếp Parent-Child với Props và Emits
Composition API hiện đại hóa cách khai báo props và event với defineProps và defineEmits, mang lại khả năng tích hợp TypeScript tốt hơn.
<script setup>
// defineProps: declares expected props
// With default values via withDefaults
const props = withDefaults(defineProps<{
title: string
count?: number
items?: string[]
}>(), {
count: 0,
items: () => []
})
// defineEmits: declares emitted events
// Precise payload typing
const emit = defineEmits<{
(e: 'update', value: number): void
(e: 'submit', data: { title: string; items: string[] }): void
}>()
// Function that emits an event
const handleSubmit = () => {
emit('submit', {
title: props.title,
items: props.items
})
}
const increment = () => {
emit('update', props.count + 1)
}
</script>
<template>
<div class="card">
<h3>{{ title }}</h3>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
<button @click="handleSubmit">Submit</button>
</div>
</template><script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
const currentCount = ref(0)
const items = ref(['Item 1', 'Item 2'])
// Handler for update event
const onUpdate = (newValue) => {
currentCount.value = newValue
}
// Handler for submit event
const onSubmit = (data) => {
console.log('Data submitted:', data)
}
</script>
<template>
<ChildComponent
title="My Component"
:count="currentCount"
:items="items"
@update="onUpdate"
@submit="onSubmit"
/>
</template>Sẵn sàng chinh phục phỏng vấn Vue.js / Nuxt.js?
Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.
Provide và Inject cho Dependency Injection
Để chia sẻ dữ liệu giữa các component ở xa nhau mà không cần prop drilling, Vue 3 cung cấp provide và inject.
<script setup>
import { provide, ref, readonly } from 'vue'
// Global theme state
const theme = ref('light')
// Function to toggle theme
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
// provide: makes available to descendants
// readonly prevents direct modification by children
provide('theme', readonly(theme))
provide('toggleTheme', toggleTheme)
</script>
<template>
<div :class="theme">
<slot />
</div>
</template><script setup>
import { inject } from 'vue'
// inject: retrieves value provided by an ancestor
// Default value if not found
const theme = inject('theme', 'light')
const toggleTheme = inject('toggleTheme', () => {})
</script>
<template>
<div>
<p>Current theme: {{ theme }}</p>
<button @click="toggleTheme">
Switch to {{ theme === 'light' ? 'dark' : 'light' }}
</button>
</div>
</template>Kết luận
Vue 3 Composition API mang đến cách tiếp cận mạnh mẽ và linh hoạt để tổ chức mã ứng dụng Vue. Các khái niệm chính cần ghi nhớ:
- ref cho giá trị nguyên thủy, reactive cho đối tượng phức tạp
- computed cho giá trị suy diễn với caching tự động
- watch và watchEffect cho các tác dụng phụ reactive
- Composable để trích xuất và tái sử dụng logic giữa các component
- Lifecycle hook dạng hàm (
onMounted,onUnmounted, v.v.) - provide/inject cho dependency injection không cần prop drilling
Cách tiếp cận này giúp việc tạo các ứng dụng dễ bảo trì và dễ kiểm thử trở nên thuận tiện, đồng thời cung cấp khả năng tích hợp TypeScript tuyệt vời. Bước tiếp theo là khám phá các pattern nâng cao như async composable và tích hợp với Pinia để quản lý state toàn cục.
Bắt đầu luyện tập!
Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.
Thẻ
Chia sẻ
