The Composition API is the modern way to write Vue components. Instead of organizing code by options (data, methods, computed, watch), you organize code by logical concern β keeping related logic together. This makes components easier to read, reuse, and maintain.
Options API vs Composition API
- Code split by type (data, methods, computed)
- Related logic scattered across sections
- Hard to extract reusable logic
- Uses
thiskeyword
- Code grouped by feature
- Related logic stays together
- Easy to extract into composables
- No
thisβ plain variables
1. script setup
<script setup> is the recommended way to use the Composition API in Single File Components. Everything declared at the top level is automatically available in the template.
<script setup>
import { ref } from 'vue'
const message = ref('Hello Vue 3!')
const count = ref(0)
function increment() {
count.value++
}
</script>
<template>
<h1>{{ message }}</h1>
<p>Count: {{ count }}</p>
<button @click="increment">+1</button>
</template><script setup>, all variables, functions, and imports are automatically exposed to the template. No need for export default or return. 2. Reactive State: ref() & reactive()
ref() β for primitives & any value
ref() wraps a value in a reactive object. Access it with .value in script, but directly in template.
import { ref } from 'vue'
// String
const name = ref('John')
// Number
const count = ref(0)
// Boolean
const isVisible = ref(true)
// Array
const items = ref(['Apple', 'Banana', 'Cherry'])
// In script: use .value
count.value++
name.value = 'Jane'
items.value.push('Date')
// In template: no .value needed!
// {{ count }} {{ name }} {{ items }}reactive() β for objects
reactive() makes an entire object reactive. No .value needed, but it only works with objects.
import { reactive } from 'vue'
const user = reactive({
name: 'John',
age: 25,
address: {
city: 'Phnom Penh',
country: 'Cambodia'
}
})
// No .value needed!
user.name = 'Jane'
user.age = 26
user.address.city = 'Siem Reap'- Works with any type
- Need
.valuein script - Can reassign entirely
- Recommended for most cases
- Only objects/arrays
- No
.valueneeded - Cannot reassign the whole object
- Good for complex state objects
3. Computed Properties
computed() creates a cached value that automatically updates when its dependencies change. Use it for derived data.
import { ref, computed } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
// Computed β auto-updates when firstName or lastName changes
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`
})
// Filtered list example
const todos = ref([
{ text: 'Learn Vue', done: true },
{ text: 'Build app', done: false },
{ text: 'Deploy', done: false },
])
const activeTodos = computed(() => {
return todos.value.filter(t => !t.done)
})
// activeTodos.value = [{ text: 'Build app' }, { text: 'Deploy' }]4. Watchers
watch() runs a callback when a reactive value changes. Use it for side effects (API calls, logging, etc).
import { ref, watch, watchEffect } from 'vue'
const search = ref('')
const userId = ref(1)
// Watch a single ref
watch(search, (newValue, oldValue) => {
console.log(`Search changed: "${oldValue}" β "${newValue}"`)
})
// Watch with options
watch(search, (newValue) => {
// Call API after user stops typing
fetchResults(newValue)
}, { debounce: 300 })
// Watch multiple sources
watch([firstName, lastName], ([newFirst, newLast]) => {
console.log(`Name: ${newFirst} ${newLast}`)
})
// Deep watch (for objects)
watch(user, (newUser) => {
console.log('User changed:', newUser)
}, { deep: true })
// watchEffect β auto-tracks dependencies
watchEffect(() => {
// Runs immediately, then re-runs when userId changes
console.log(`Fetching user ${userId.value}`)
fetchUser(userId.value)
})5. Lifecycle Hooks
Lifecycle hooks let you run code at specific points in a component's life.
| Hook | When it runs | Common use |
|---|---|---|
| onMounted | After DOM is rendered | Fetch data, init libraries |
| onUpdated | After reactive state change causes re-render | DOM-dependent operations |
| onUnmounted | Component is removed | Cleanup (timers, listeners) |
| onBeforeMount | Before DOM is rendered | Pre-render logic |
| onBeforeUnmount | Before component is removed | Save state, cleanup |
import { onMounted, onUnmounted, ref } from 'vue'
const data = ref(null)
let intervalId = null
// Runs after component is mounted to DOM
onMounted(async () => {
// Fetch data from API
const res = await fetch('/api/data')
data.value = await res.json()
// Start a timer
intervalId = setInterval(() => {
console.log('tick')
}, 1000)
})
// Cleanup when component is destroyed
onUnmounted(() => {
clearInterval(intervalId)
})6. Props & Emits
Props β receive data from parent
// Child component: UserCard.vue
<script setup>
// Define what props this component accepts
const props = defineProps<{
name: string
age: number
avatar?: string // optional prop
}>()
// Use props directly
console.log(props.name)
</script>
<template>
<div class="card">
<img :src="avatar" />
<h2>{{ name }}</h2>
<p>Age: {{ age }}</p>
</div>
</template>Emits β send events to parent
// Child component: SearchInput.vue
<script setup>
const emit = defineEmits<{
search: [query: string]
clear: []
}>()
const query = ref('')
function onSearch() {
emit('search', query.value)
}
function onClear() {
query.value = ''
emit('clear')
}
</script>
<template>
<input v-model="query" @keyup.enter="onSearch" />
<button @click="onSearch">Search</button>
<button @click="onClear">Clear</button>
</template>Using the component
<!-- Parent component -->
<template>
<UserCard
name="John"
:age="25"
avatar="/john.jpg"
/>
<SearchInput
@search="handleSearch"
@clear="handleClear"
/>
</template>
<script setup>
function handleSearch(query) {
console.log('Searching for:', query)
}
function handleClear() {
console.log('Search cleared')
}
</script>7. Composables (Reusable Logic)
Composables are functions that encapsulate and reuse stateful logic. Convention: name them use[Something].
// composables/useCounter.ts
import { ref, computed } from 'vue'
export function useCounter(initial = 0) {
const count = ref(initial)
const doubled = computed(() => count.value * 2)
const isPositive = computed(() => count.value > 0)
function increment() { count.value++ }
function decrement() { count.value-- }
function reset() { count.value = initial }
return { count, doubled, isPositive, increment, decrement, reset }
}<script setup>
import { useCounter } from '~/composables/useCounter'
// Use the composable β each instance has its own state
const { count, doubled, increment, decrement, reset } = useCounter(10)
// Can use multiple composables
const likes = useCounter(0)
const views = useCounter(100)
</script>
<template>
<p>Count: {{ count }} (doubled: {{ doubled }})</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
<button @click="reset">Reset</button>
</template>useFetch(), useAuth(), useTheme(), useLocalStorage(). Composables are Vue's answer to React hooks. 8. Template Refs
Access DOM elements directly using ref + template ref attribute.
<script setup>
import { ref, onMounted } from 'vue'
// Create a ref with null initial value
const inputRef = ref<HTMLInputElement | null>(null)
onMounted(() => {
// Access the DOM element after mount
inputRef.value?.focus()
})
function selectAll() {
inputRef.value?.select()
}
</script>
<template>
<!-- Connect with ref="inputRef" -->
<input ref="inputRef" type="text" placeholder="Auto-focused!" />
<button @click="selectAll">Select All</button>
</template>9. Provide / Inject
Pass data deep through component trees without prop drilling.
<!-- Parent.vue -->
<script setup>
import { provide, ref } from 'vue'
const theme = ref('dark')
const user = ref({ name: 'John', role: 'admin' })
// Provide values β available to ALL descendants
provide('theme', theme)
provide('user', user)
</script><!-- Any deeply nested child -->
<script setup>
import { inject } from 'vue'
// Inject values from any ancestor
const theme = inject('theme') // ref('dark')
const user = inject('user') // ref({ name: 'John', role: 'admin' })
// With default value (in case no ancestor provides it)
const lang = inject('lang', 'en')
</script>
<template>
<div :class="theme">
<p>Welcome, {{ user.name }}</p>
</div>
</template>Summary
- βscript setup β cleaner syntax, auto-exposed variables
- βref() / reactive() β make data reactive
- βcomputed() β cached derived values
- βwatch() β react to changes with side effects
- βLifecycle hooks β onMounted, onUnmounted, etc.
- βdefineProps / defineEmits β component communication
- βComposables β reusable stateful logic (use[Something])
- βprovide / inject β pass data without prop drilling