Vue 3 Composition API in Practice
Since Vue 3.0's release, the Composition API has evolved from "new feature" to Vue's default development paradigm. It solves the core pain points of Options API in large components — scattered logic and poor reusability — while preserving Vue's characteristic elegance and intuitiveness.
ref vs reactive: How to Choose?
This is the most common question from beginners. Essentially, reactive fits object-shaped state, while ref fits primitive types and objects that "need to be replaced wholesale."
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
export interface Product {
id: number
name: string
price: number
category: string
tags: string[]
rating: number
inStock: boolean
}
export interface FilterOptions {
keyword: string
categories: string[]
priceRange: [number, number]
minRating: number
inStockOnly: boolean
}
export function useProductFilter(initialProducts: Product[]) {
const products = ref<Product[]>(initialProducts)
const loading = ref(false)
const error = ref<string | null>(null)
const filters = reactive<FilterOptions>({
keyword: '',
categories: [],
priceRange: [0, 10000],
minRating: 0,
inStockOnly: false
})
const filteredProducts = computed(() => {
let result = [...products.value]
if (filters.keyword) {
const kw = filters.keyword.toLowerCase()
result = result.filter(p => p.name.toLowerCase().includes(kw))
}
if (filters.categories.length > 0) {
result = result.filter(p => filters.categories.includes(p.category))
}
result = result.filter(p =>
p.price >= filters.priceRange[0] &&
p.price <= filters.priceRange[1]
)
if (filters.minRating > 0) {
result = result.filter(p => p.rating >= filters.minRating)
}
if (filters.inStockOnly) {
result = result.filter(p => p.inStock)
}
return result
})
const stats = computed(() => ({
total: filteredProducts.value.length,
maxPrice: Math.max(0, ...filteredProducts.value.map(p => p.price)),
avgRating: filteredProducts.value.length
? filteredProducts.value.reduce((s, p) => s + p.rating, 0) / filteredProducts.value.length
: 0
}))
function resetFilters() {
filters.keyword = ''
filters.categories = []
filters.priceRange = [0, 10000]
filters.minRating = 0
filters.inStockOnly = false
}
function toggleCategory(cat: string) {
const i = filters.categories.indexOf(cat)
i >= 0 ? filters.categories.splice(i, 1) : filters.categories.push(cat)
}
let unwatch: (() => void) | null = null
onMounted(() => {
unwatch = watch(
() => [filters.keyword, filters.priceRange[1]],
async () => {
// 模拟统计上报
console.log('[analytics] filter changed')
},
{ deep: false, flush: 'post' }
)
})
onBeforeUnmount(() => {
unwatch?.()
})
return {
products,
loading,
error,
filters,
filteredProducts,
stats,
resetFilters,
toggleCategory
}
}
Two Responsive Declarations Compared
| Dimension | ref | reactive |
|---|---|---|
| Primitive types supported | ✅ string/number/boolean etc | ❌ Objects/arrays only |
| Access pattern | .value for read/write |
Direct property access |
| Destructuring behavior | ✅ Retains reactivity | ❌ Loses reactivity (needs toRefs) |
| Whole replacement | obj.value = newObj safe |
Manual Object.assign merge |
| TS inference | Intuitive Ref<T> |
Recursive unwrap confusing |
| Memory overhead | Slightly higher (wrapper object) | Slightly lower (direct Proxy wrap) |
Three Golden Rules for Composable Extraction
The useProductFilter in the code example is a typical Composable. A good Composable should follow:
- Single Responsibility: Each Composable does one thing. Never write monstrosities like
useProductFilterWithAuthAndAnalytics - Explicit Returns: Use
return { a, b, c }rather than returning reactive objects; let callers see exactly what they're using - Lifecycle Symmetry: Resources claimed inside the Composable (watchers, timers, event subscriptions) must always be released in onBeforeUnmount
Avoid Common Performance Pitfalls
The Composition API is flexible but also full of pitfalls:
// ❌ Wrong: Recomputes huge array every render
const sorted = [...products.value].sort(heavySortFn)
// ✅ Correct: Cache result with computed
const sorted = computed(() => [...products.value].sort(heavySortFn))
// ❌ Wrong: Deep watching huge objects
watch(hugeObject, handler, { deep: true })
// ✅ Better: Watch only the field you care about
watch(() => hugeObject.items[0].status, handler)
Best Practices
- Wrap all derived state in
computed— never write complex expressions inside templates - Uniform naming: Composable functions use
useXxxprefix; filenames match asuseXxx.ts - Combine Provide/Inject with Composables for unified global state management in root-level Provider components
- Don't put template-specific computation logic in Composables — keep them generally applicable
- Use
unref()andisRef()to build utility functions that accept both Refs and plain values
Summary
The core value of the Composition API lies in "logical concern aggregation" and "fine-grained reuse granularity." Its real power manifests when you reorganize the data/methods/computed/watch scattered across the Options API era into testable, composable units organized by business domain. Mastering Composable design will bring a qualitative leap to your Vue code quality.