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.