Vue.js 3 Composition API
Vue.js 3 Composition API: A Modern Approach to Component Logic
The Composition API is one of Vue 3's most significant additions, giving developers a more flexible and scalable way to organize component logic. While the traditional Options API groups code by property type (data, methods, computed), the Composition API lets you group logic by concern, making components easier to read, test, and reuse. In this article, we'll explore the core concepts, practical patterns, and best practices for writing clean, maintainable Vue components with the Composition API.
Why the Composition API Matters
Before diving into syntax, it's important to understand why the Composition API exists. In large Vue applications, components written with the Options API often become difficult to maintain. Related logic gets scattered across data, methods, and computed properties, making it hard to trace functionality. The Composition API addresses this by allowing you to co-locate related code, improving both readability and reusability.
For example, if you have a component that handles user authentication, all the logic related to login state, API calls, and validation can live together in a single composable function rather than being split across multiple options. This is especially valuable in larger projects where components can grow to hundreds of lines.
Core Concepts: ref, reactive, and computed
At the heart of the Composition API are reactive primitives. The two most commonly used are ref and reactive. While they serve similar purposes, they have different use cases.
Using ref for Primitive Values
ref is ideal for storing primitive values like strings, numbers, or booleans. It wraps the value in an object with a .value property, ensuring reactivity. Here's a simple example:
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
const increment = () => {
count.value++
}
return {
count,
increment
}
}
}
In the template, you can use count directly without .value:
<template>
<button @click="increment">{{ count }}</button>
</template>
Using reactive for Objects and Arrays
reactive works best with objects and arrays. It creates a deeply reactive proxy of the original object, so nested properties are automatically tracked:
import { reactive } from 'vue'
export default {
setup() {
const user = reactive({
name: 'Alex',
role: 'developer'
})
return { user }
}
}
Creating Derived State with computed
computed properties are perfect for deriving values from other reactive state. They're memoized, meaning they only recalculate when their dependencies change:
import { ref, computed } from 'vue'
export default {
setup() {
const firstName = ref('John')
const lastName = ref('Doe')
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`
})
return { firstName, lastName, fullName }
}
}
Understanding setup() and the Return Object
The setup() function is the entry point for the Composition API. It runs before the component is mounted and provides access to the component's context. Any properties returned from setup() become available in the template.
import { ref } from 'vue'
export default {
setup() {
const message = ref('Hello from Composition API')
// These are exposed to the template
return {
message
}
}
}
It's important to note that this is not available inside setup(). Instead, you work directly with reactive primitives and functions. This makes the code more explicit and easier to reason about.
Composables: Reusable Logic with the Composition API
Composables are one of the most powerful features of the Composition API. They allow you to extract and reuse stateful logic across components. A composable is simply a function that uses Vue's reactive primitives and returns reactive state or methods.
Writing Your First Composable
Let's create a composable for managing a list of items:
// useItemList.js
import { ref, computed } from 'vue'
export function useItemList(initialItems = []) {
const items = ref([...initialItems])
const addItem = (item) => {
items.value.push(item)
}
const removeItem = (index) => {
items.value.splice(index, 1)
}
const filteredItems = computed(() => {
return items.value.filter(item => item.active)
})
return {
items,
filteredItems,
addItem,
removeItem
}
}
Then, use it in any component:
// MyComponent.vue
import { useItemList } from './useItemList'
export default {
setup() {
const { items, filteredItems, addItem, removeItem } = useItemList([
{ id: 1, name: 'Item 1', active: true },
{ id: 2, name: 'Item 2', active: false }
])
return {
items,
filteredItems,
addItem,
removeItem
}
}
}
Naming Conventions
By convention, composable functions should start with use and be placed in a composables/ directory. This makes them easy to find and import across your project.
Lifecycle Hooks in the Composition API
Vue provides lifecycle hooks that you can use inside setup(). Instead of using options like mounted or created, you import the hooks from Vue:
import { onMounted, onUnmounted, ref } from 'vue'
export default {
setup() {
const element = ref(null)
onMounted(() => {
console.log('Component mounted')
})
onUnmounted(() => {
console.log('Component unmounted')
})
return { element }
}
}
This approach makes it clear which lifecycle events are being handled and keeps the logic grouped with the related state.
Best Practices for the Composition API
To write clean, maintainable code with the Composition API, follow these guidelines:
- Group related logic together. Keep state, methods, and computed properties that work together in the same scope or composable.
- Extract reusable logic into composables. If you find yourself duplicating similar patterns across components, move that logic into a composable.
- Use
readonlyfor props. Prevent accidental mutations by using Vue'sreadonlyutility:
import { readonly } from 'vue'
const props = defineProps({
title: String
})
const safeProps = readonly(props)
- Keep
setup()focused. Avoid putting too much logic insetup(). Break complex functionality into smaller, focused composables.
- Type your composables. If you're using TypeScript, define interfaces for your composable inputs and outputs to catch errors early:
interface UserState {
name: string
age: number
}
export function useUser(initialUser: UserState) {
const user = reactive(initialUser)
const updateAge = (newAge: number) => {
user.age = newAge
}
return { user, updateAge }
}
Common Pitfalls and How to Avoid Them
While the Composition API offers great flexibility, there are common mistakes to watch out for:
- Forgetting to return values from
setup(). Any state or method you want to use in the template must be explicitly returned.
- Using
thisinsetup(). Thethiskeyword is not available insetup(). Use the reactive primitives and functions you return instead.
- Creating unnecessary reactivity. Not every variable needs to be reactive. Use
refonly when you need reactivity; otherwise, plain variables are fine.
- Overusing composables. Not every piece of logic belongs in a composable. Keep composables focused on reusable, shared state, and don't create them for single-component logic.
Migration from Options API
If you're working with an existing Vue 2 application, migrating to the Composition API doesn't require a complete rewrite. Vue 3 supports both APIs side by side, so you can gradually refactor components as needed. Many teams choose to start with new components using the Composition API while maintaining legacy components in the Options API.
Conclusion
The Vue.js 3 Composition API represents a significant evolution in how we write components. By grouping logic by concern rather than by option type, it makes code more readable, testable, and reusable. Whether you're starting a new project or migrating an existing one, mastering the Composition API will help you build more maintainable Vue applications. Start small by extracting reusable logic into composables, and gradually adopt the patterns that work best for your team's workflow.