Vue 3 Learning Notes: Composition API, Reactivity, and Patterns I Use Daily
Why Vue 3 Changed Everything
Vue 3 wasn’t just an incremental update. The Composition API fundamentally changed how we think about component logic. After months of daily use, here are the patterns and concepts that actually matter.
Composition API vs Options API
The Options API splits logic by option type — data, methods, computed, watch. The Composition API splits logic by concern.
<script setup>
// All related logic lives together
const count = ref(0)
const doubled = computed(() => count.value * 2)
function increment() {
count.value++
}
watch(count, (newVal) => {
console.log('Count changed:', newVal)
})
</script>This matters when components grow. You can extract related logic into composables instead of scattering it across options.
ref vs reactive
This confused me for weeks. Here’s the simple rule:
// ref — primitives and reassignable values
const count = ref(0)
const name = ref('Jay')
// reactive — objects (no .value needed)
const state = reactive({ count: 0, name: 'Jay' })When to use which:
- Use
reffor most things. It’s explicit and works with primitives. - Use
reactivefor complex objects that won’t be reassigned. - Never destructure
reactive— it breaks reactivity.
// ❌ Breaks reactivity
const { count } = reactive({ count: 0 })
// ✅ Works
const state = reactive({ count: 0 })
const count = toRef(state, 'count')Composables: The Real Power
Composables are Vue 3’s answer to mixins. They’re just functions that use reactive state.
export function useCounter(initial = 0) {
const count = ref(initial)
const doubled = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubled, increment }
}<script setup>
const { count, doubled, increment } = useCounter(10)
</script>Why this is better than mixins:
- No name collisions
- Explicit inputs/outputs
- Easy to test
- TypeScript-friendly
Watch Patterns
Vue 3 gives you fine-grained watchers:
// Watch a ref
watch(count, (newVal, oldVal) => {
console.log(`${oldVal} → ${newVal}`)
})
// Watch multiple sources
watch([count, name], ([newCount, newName]) => {
console.log(newCount, newName)
})
// Watch reactive object property
watch(() => state.count, (newVal) => {
console.log(newVal)
})
// Deep watch
watch(state, (newVal) => {
console.log('Anything changed')
}, { deep: true })
// Immediate execution
watch(count, (newVal) => {
console.log(newVal)
}, { immediate: true })script setup: The Standard
<script setup> is the recommended way to write Vue 3 components. It’s less boilerplate, better TypeScript inference, and cleaner code.
<script setup>
// Imports are automatically available in template
import { ref, computed } from 'vue'
import ChildComponent from './ChildComponent.vue'
const props = defineProps({
title: String,
count: { type: Number, default: 0 }
})
const emit = defineEmits(['update'])
const localState = ref(0)
</script>
<template>
<h1>{{ title }}</h1>
<ChildComponent :count="localState" />
</template>Real-World Pattern: useApi Composable
Here’s a composable I use in production:
export function useApi<T>(url: string) {
const data = ref<T | null>(null)
const error = ref<string | null>(null)
const loading = ref(false)
async function fetch() {
loading.value = true
error.value = null
try {
const res = await window.fetch(url)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
data.value = await res.json()
} catch (e) {
error.value = e instanceof Error ? e.message : 'Unknown error'
} finally {
loading.value = false
}
}
return { data, error, loading, fetch }
}Usage:
<script setup>
const { data: users, loading, fetch: loadUsers } = useApi('/api/users')
onMounted(() => loadUsers())
</script>
<template>
<div v-if="loading">Loading...</div>
<div v-else-if="error">{{ error }}</div>
<ul v-else>
<li v-for="user in users" :key="user.id">{{ user.name }}</li>
</ul>
</template>Key Takeaways
- Use
refby default — it’s explicit and flexible - Composables over mixins — extract, reuse, test
<script setup>everywhere — less boilerplate, better DX- Watch specific values — avoid deep watches when possible
- Don’t destructure reactive objects — use
toRef/toRefsinstead
Vue 3’s Composition API isn’t just a new syntax. It’s a better mental model for building components. Once it clicks, going back feels wrong.