The Composition API Seems Weird at First
I remember looking at it for the first time and thinking "why would I want this?" The Options API felt natural - data goes here, methods go there, computed properties over here.
Then I built something with more than 200 lines in a component and understood.
The Real Benefit
With Options API, related logic gets scattered. Your search feature has its data in data(), its methods in methods, its computed values in computed, and its watchers in watch. You're constantly jumping around.
Composition API lets you keep related stuff together.
Script Setup is the Way
Skip the verbose setup() function. Use <script setup>:
<script setup>
import { ref, computed } from 'vue'
const count = ref(0)
const doubled = computed(() => count.value * 2)
function increment() {
count.value++
}
</script>
<template>
<button @click="increment">{{ count }} ({{ doubled }})</button>
</template>
Everything you declare is automatically available in the template. No more returning objects.
ref vs reactive
Here's the simple rule:
ref() for primitives (strings, numbers, booleans):
const name = ref('Matt')
const count = ref(0)
const isLoading = ref(false)
// Access with .value in script
name.value = 'New Name'
// Template unwraps automatically
// {{ name }} just works
reactive() for objects:
const user = reactive({
name: 'Matt',
email: 'matt@example.com'
})
// Direct access, no .value
user.name = 'Updated'
I mostly use ref() for everything now. It's more consistent and you don't have to think about whether something is reactive or a ref.
Computed Properties
Same concept as before, different syntax:
const items = ref([])
const completedCount = computed(() => {
return items.value.filter(item => item.done).length
})
Computed values cache automatically - they only recalculate when their dependencies change.
Watching Things
import { watch, watchEffect } from 'vue'
// Watch specific value
watch(searchQuery, (newVal, oldVal) => {
console.log('Search changed:', newVal)
fetchResults(newVal)
})
// Watch multiple
watch([firstName, lastName], ([first, last]) => {
fullName.value = `${first} ${last}`
})
watchEffect is useful when you want to track dependencies automatically:
watchEffect(() => {
// This runs immediately and re-runs whenever userId changes
fetchUser(userId.value)
})
Making Composables
This is where Composition API shines. Extract reusable logic:
// composables/useSearch.js
import { ref, watch } from 'vue'
export function useSearch(fetchFn) {
const query = ref('')
const results = ref([])
const loading = ref(false)
watch(query, async (value) => {
if (!value) {
results.value = []
return
}
loading.value = true
results.value = await fetchFn(value)
loading.value = false
}, { debounce: 300 })
return { query, results, loading }
}
Use it anywhere:
<script setup>
import { useSearch } from '@/composables/useSearch'
const { query, results, loading } = useSearch(
(q) => fetch(`/api/search?q=${q}`).then(r => r.json())
)
</script>
Props and Events
<script setup>
const props = defineProps({
title: String,
count: {
type: Number,
default: 0
}
})
const emit = defineEmits(['update', 'delete'])
function save() {
emit('update', { title: props.title })
}
</script>
Lifecycle Hooks
import { onMounted, onUnmounted } from 'vue'
onMounted(() => {
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
Stop Overthinking It
You don't need to refactor every component to use Composition API. Use it when:
- Components get big and messy
- You want to share logic between components
- You're building something new
The Options API still works fine. Pick what makes your code clearer.
