Learn how to leverage the power of Vue 3's Composition API to build more maintainable and scalable applications.
The introduction of the Composition API in Vue 3 represents one of the most significant changes to the framework. It offers a more flexible and powerful way to organize component logic, especially in complex applications. This guide will help you understand and master this essential feature.
The Composition API was introduced to address limitations in Vue 2's Options API, particularly for reusing logic across components and for better TypeScript support. It allows you to organize code by logical concerns rather than by option types.
Let's look at a basic component using the Composition API:
<script setup>
import { ref, onMounted } from 'vue'
// Reactive state
const count = ref(0)
// Functions that mutate state and trigger updates
function increment() {
count.value++
}
// Lifecycle hooks
onMounted(() => {
console.log(`The initial count is ${count.value}.`)
})
</script>
<template>
<button @click="increment">Count is: {{ count }}</button>
</template>
The Composition API provides several reactivity APIs:
ref
: Creates a reactive reference for primitive valuesreactive
: Creates a reactive objectcomputed
: Creates a computed propertywatch
/watchEffect
: For side effects based on reactive state changes<script setup>
import { reactive, computed } from 'vue'
const state = reactive({
firstName: 'John',
lastName: 'Doe',
age: 30
})
// Computed property
const fullName = computed(() => `${state.firstName} ${state.lastName}`)
// Method that modifies state
function incrementAge() {
state.age++
}
</script>
<template>
<div>
<p>Name: {{ fullName }}</p>
<p>Age: {{ state.age }}</p>
<button @click="incrementAge">Increment Age</button>
</div>
</template>
One of the biggest advantages of the Composition API is the ability to extract and reuse logic through "composables" - functions that encapsulate and return reactive state and methods.
// useCounter.js
import { ref } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
function increment() {
count.value++
}
function decrement() {
count.value--
}
return {
count,
increment,
decrement
}
}
<script setup>
import { useCounter } from './useCounter'
const { count, increment, decrement } = useCounter(10)
</script>
<template>
<div>
<button @click="decrement">-</button>
<span>{{ count }}</span>
<button @click="increment">+</button>
</div>
</template>
Accessing DOM elements is straightforward with the Composition API:
<script setup>
import { ref, onMounted } from 'vue'
const inputRef = ref(null)
onMounted(() => {
inputRef.value.focus()
})
</script>
<template>
<input ref="inputRef" />
</template>
Handling props is slightly different with the Composition API:
<script setup>
import { computed } from 'vue'
// Define props with types and defaults
const props = defineProps({
title: {
type: String,
required: true
},
count: {
type: Number,
default: 0
}
})
// Computed property based on props
const capitalizedTitle = computed(() =>
props.title.charAt(0).toUpperCase() + props.title.slice(1)
)
// Define events
const emit = defineEmits(['update:count'])
function incrementCount() {
emit('update:count', props.count + 1)
}
</script>
<template>
<div>
<h2>{{ capitalizedTitle }}</h2>
<p>Count: {{ count }}</p>
<button @click="incrementCount">Increment</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const userData = ref(null)
const loading = ref(false)
const error = ref(null)
async function fetchUserData(userId) {
loading.value = true
error.value = null
try {
const response = await fetch(`https://api.example.com/users/${userId}`)
userData.value = await response.json()
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
}
</script>
The Composition API makes it easier to create renderless components:
<!-- FetchData.vue -->
<script setup>
import { ref, onMounted, provide } from 'vue'
const props = defineProps({
url: String,
immediate: {
type: Boolean,
default: true
}
})
const data = ref(null)
const loading = ref(false)
const error = ref(null)
async function fetchData() {
loading.value = true
error.value = null
try {
const response = await fetch(props.url)
data.value = await response.json()
} catch (e) {
error.value = e
} finally {
loading.value = false
}
}
// Provide context to slot
provide('fetchState', {
data,
loading,
error,
refetch: fetchData
})
onMounted(() => {
if (props.immediate) {
fetchData()
}
})
</script>
<template>
<slot :data="data" :loading="loading" :error="error" :refetch="fetchData" />
</template>
Usage:
<template>
<FetchData url="https://api.example.com/posts">
<template v-slot="{ data, loading, error, refetch }">
<div v-if="loading">Loading...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<div v-else>
<ul>
<li v-for="post in data" :key="post.id">{{ post.title }}</li>
</ul>
<button @click="refetch">Refresh</button>
</div>
</template>
</FetchData>
</template>
The Composition API offers a more flexible way to organize component logic, especially for complex applications. By focusing on function-based composition rather than options-based organization, you can create more maintainable and reusable code.
Key takeaways:
ref
for primitive values and reactive
for objectsWith these fundamentals in place, you're well on your way to mastering Vue 3's Composition API and building more scalable applications.
Web Developer
Full-stack web developer with expertise in Vue.js, Laravel, and modern JavaScript frameworks.