How to Use Composition Api in Vue
How to Use Composition API in Vue The Vue.js framework has evolved significantly since its initial release, introducing powerful patterns that improve code organization, reusability, and scalability. One of the most transformative additions in Vue 3 is the Composition API . Unlike the Options API, which organizes logic by options (data, methods, computed, etc.), the Composition API lets developers
How to Use Composition API in Vue
The Vue.js framework has evolved significantly since its initial release, introducing powerful patterns that improve code organization, reusability, and scalability. One of the most transformative additions in Vue 3 is the Composition API. Unlike the Options API, which organizes logic by options (data, methods, computed, etc.), the Composition API lets developers group related logic by feature or concern making complex components easier to read, maintain, and test.
As applications grow in size and complexity, managing state, side effects, and logic across multiple components becomes increasingly challenging. The Composition API directly addresses these challenges by enabling developers to write more modular, reusable, and predictable code. Whether you're building a small dashboard or a large enterprise application, understanding how to use the Composition API effectively is no longer optional it's essential for modern Vue development.
In this comprehensive guide, youll learn how to use the Composition API in Vue from the ground up. Well walk through practical implementation steps, explore industry best practices, highlight essential tools, showcase real-world examples, and answer common questions. By the end of this tutorial, youll be equipped to refactor existing components, write new ones with confidence, and leverage the full power of Vue 3s most innovative feature.
Step-by-Step Guide
Setting Up a Vue 3 Project with Composition API
Before diving into the Composition API, ensure your project is running Vue 3. The Composition API is not available in Vue 2 unless you install the @vue/composition-api plugin, which is now deprecated. For new projects, use the official Vue CLI or Vite to scaffold a Vue 3 application.
To create a new project using Vite the recommended build tool for Vue 3 run the following command in your terminal:
npm create vue@latest
Follow the prompts to select options like TypeScript, ESLint, and testing tools. Once the project is created, navigate into the directory and install dependencies:
cd my-vue-app
npm install
Start the development server:
npm run dev
By default, Vue 3 projects created with Vite use the Composition API in all new components. Youll notice that the default App.vue file uses the
Understanding the setup() Function
The heart of the Composition API is the setup() function. It is called before the component is created, once the props have been resolved, and serves as the entry point for using reactive state, computed properties, methods, and lifecycle hooks.
Heres a basic example using the traditional setup() syntax:
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
const increment = () => {
count.value++
}
return {
count,
increment
}
}
}
</script>
In this example:
ref(0)creates a reactive reference with an initial value of 0.- The
incrementfunction modifies thecountvalue. - All values and functions that need to be accessible in the template must be returned from
setup().
While this syntax works, Vue 3 introduced the <script setup> syntax to reduce boilerplate. Heres the same component rewritten:
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
const increment = () => {
count.value++
}
</script>
Notice how theres no need to explicitly return count and increment. The <script setup> macro automatically makes all top-level bindings available in the template. This is now the recommended approach for most use cases.
Using Reactive State with ref() and reactive()
Two core functions for managing state in the Composition API are ref() and reactive().
ref() is used to create a reactive reference to a primitive value (string, number, boolean) or an object. When you access or modify a ref value in the template or JavaScript, you must use .value.
<script setup>
import { ref } from 'vue'
const message = ref('Hello, Composition API!')
const userAge = ref(25)
const updateMessage = () => {
message.value = 'Updated via ref!'
}
</script>
reactive() is used to create a reactive object. Unlike ref(), you do not need to use .value to access its properties it behaves like a regular JavaScript object, but all properties are reactive.
<script setup>
import { reactive } from 'vue'
const user = reactive({
name: 'Alice',
email: 'alice@example.com',
isActive: true
})
const updateUser = () => {
user.name = 'Bob'
user.isActive = false
}
</script>
Important: reactive() only works on objects. If you try to use it on a primitive value, it wont be reactive. For primitives, always use ref().
Working with Computed Properties
Computed properties are values derived from other reactive state. They are cached based on their dependencies and only re-evaluate when those dependencies change.
In the Composition API, use the computed() function:
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
const fullName = computed(() => {
return ${firstName.value} ${lastName.value}
})
// fullName will update automatically when firstName or lastName changes
</script>
<template>
<p>Full Name: {{ fullName }}</p>
</template>
Computed properties are ideal for expensive calculations, filtering lists, or formatting data. They improve performance by avoiding unnecessary recalculations.
Handling Events and Methods
Methods in the Composition API are simply JavaScript functions defined within setup() or <script setup>. They can access reactive state and other functions directly.
<script setup>
import { ref } from 'vue'
const items = ref(['Apple', 'Banana', 'Cherry'])
const searchTerm = ref('')
const filteredItems = computed(() => {
return items.value.filter(item =>
item.toLowerCase().includes(searchTerm.value.toLowerCase())
)
})
const addItem = () => {
if (searchTerm.value.trim()) {
items.value.push(searchTerm.value)
searchTerm.value = ''
}
}
const removeItem = (index) => {
items.value.splice(index, 1)
}
</script>
<template>
<input v-model="searchTerm" placeholder="Search items..." />
<button @click="addItem">Add Item</button>
<ul>
<li v-for="(item, index) in filteredItems" :key="index">
{{ item }}
<button @click="removeItem(index)">Remove</button>
</li>
</ul>
</template>
Notice how addItem and removeItem are defined as regular functions but still have full access to reactive state. This makes logic more predictable and easier to test.
Using Lifecycle Hooks
The Composition API provides functions to access Vues lifecycle hooks as first-class citizens. These functions are imported directly and called synchronously in the setup scope.
Heres a mapping of Options API hooks to their Composition API equivalents:
beforeCreate? Not needed (setup() replaces it)created? Not needed (setup() replaces it)beforeMount?onBeforeMount()mounted?onMounted()beforeUpdate?onBeforeUpdate()updated?onUpdated()beforeUnmount?onBeforeUnmount()unmounted?onUnmounted()
Example using onMounted() and onUnmounted():
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const timer = ref(null)
onMounted(() => {
timer.value = setInterval(() => {
console.log('Timer tick')
}, 1000)
})
onUnmounted(() => {
if (timer.value) {
clearInterval(timer.value)
}
})
</script>
These lifecycle functions must be called synchronously during the components setup phase they cannot be called conditionally or inside async functions unless wrapped in a watch or similar mechanism.
Working with Props and Emits
When using <script setup>, props and emits are handled using two special functions: defineProps() and defineEmits().
Define props with type annotations for better tooling support:
<script setup>
const props = defineProps({
title: String,
count: {
type: Number,
default: 0
},
isActive: Boolean
})
const emit = defineEmits(['update:count', 'delete'])
const handleDelete = () => {
emit('delete')
}
const handleCountUpdate = (newCount) => {
emit('update:count', newCount)
}
</script>
<template>
<h2>{{ title }}</h2>
<p>Count: {{ count }}</p>
<button @click="handleDelete">Delete</button>
<button @click="handleCountUpdate(count + 1)">Increment</button>
</template>
For more complex scenarios, you can also use TypeScript interfaces to define prop types:
<script setup lang="ts">
interface Props {
title: string
count: number
isActive: boolean
}
const props = withDefaults(defineProps(), {
count: 0,
isActive: true
})
const emit = defineEmits
(e: 'update:count', value: number): void
(e: 'delete'): void
}>()
</script>
This approach provides full TypeScript support, autocompletion, and compile-time type checking.
Using provide() and inject() for Dependency Injection
The Composition API improves dependency injection with the provide() and inject() functions.
In a parent component:
<script setup>
import { provide, ref } from 'vue'
const theme = ref('dark')
const toggleTheme = () => {
theme.value = theme.value === 'dark' ? 'light' : 'dark'
}
provide('theme', theme)
provide('toggleTheme', toggleTheme)
</script>
In a child or deeply nested component:
<script setup>
import { inject } from 'vue'
const theme = inject('theme')
const toggleTheme = inject('toggleTheme')
// Now you can use theme and toggleTheme in template or logic
</script>
<template>
<button @click="toggleTheme">Toggle Theme: {{ theme }}</button>
</template>
For better type safety with TypeScript, define injection keys as symbols:
const themeKey = Symbol('theme')
// In parent:
provide(themeKey, theme)
// In child:
const theme = inject(themeKey)
</script>
Best Practices
Organize Logic by Feature, Not Option Type
One of the biggest advantages of the Composition API is the ability to group related logic together. Instead of scattering state, computed properties, and methods across different options, group them by feature.
Example: A user profile component with form handling, validation, and image upload logic:
<script setup>
import { ref, computed } from 'vue'
// Form state
const name = ref('')
const email = ref('')
const avatar = ref(null)
// Validation logic
const errors = computed(() => {
const err = {}
if (!name.value) err.name = 'Name is required'
if (!email.value) err.email = 'Email is required'
return err
})
const isValid = computed(() => Object.keys(errors.value).length === 0)
// Form submission
const handleSubmit = async () => {
if (!isValid.value) return
// Submit to API
}
// Image upload
const handleImageUpload = (event) => {
avatar.value = event.target.files[0]
}
</script>
This structure makes it easy to understand what logic belongs to the form feature no more jumping between data, computed, and methods sections.
Extract Reusable Logic with Custom Composables
Custom composables are functions that encapsulate and reuse stateful logic across components. They follow the naming convention of starting with use e.g., useUser(), useLocalStorage(), useFetch().
Example: A reusable useLocalStorage composable:
// composables/useLocalStorage.js
import { ref } from 'vue'
export function useLocalStorage(key, initialValue) {
const storedValue = localStorage.getItem(key)
const value = ref(storedValue ? JSON.parse(storedValue) : initialValue)
value.value = storedValue ? JSON.parse(storedValue) : initialValue
const setValue = (newValue) => {
value.value = newValue
localStorage.setItem(key, JSON.stringify(newValue))
}
return [value, setValue]
}
Usage in a component:
<script setup>
import { useLocalStorage } from '@/composables/useLocalStorage'
const [count, setCount] = useLocalStorage('count', 0)
const [theme, setTheme] = useLocalStorage('theme', 'dark')
</script>
Custom composables are testable, reusable, and promote DRY principles. They are the backbone of scalable Vue applications.
Use TypeScript for Type Safety
Vue 3 and the Composition API were designed with TypeScript in mind. Using TypeScript helps catch errors early, improves IDE support, and makes code more maintainable.
Always define types for props, emits, and state:
<script setup lang="ts">
interface User {
id: number
name: string
email: string
}
const user = ref<User | null>(null)
const emit = defineEmits
(e: 'userUpdated', user: User): void
}>()
const updateUser = (updatedUser: User) => {
user.value = updatedUser
emit('userUpdated', updatedUser)
}
</script>
Use defineProps and defineEmits with generics or interfaces for full type inference.
Avoid Deeply Nested Logic
While the Composition API allows you to group logic, avoid creating overly large setup() functions. If your component exceeds 100150 lines of logic, consider splitting into multiple composables.
Bad:
// Too much logic in one place
const setup = () => {
// Form state
// Validation
// API calls
// Event handlers
// Lifecycle hooks
// Animation logic
// Local storage sync
// ...
}
Good:
// composables/useFormValidation.js
// composables/useApi.js
// composables/useLocalStorage.js
// composables/useAnimations.js
// Component
<script setup>
import useFormValidation from '@/composables/useFormValidation'
import useApi from '@/composables/useApi'
import useLocalStorage from '@/composables/useLocalStorage'
const { errors, isValid } = useFormValidation()
const { loadData, saveData } = useApi()
const [theme, setTheme] = useLocalStorage('theme', 'dark')
</script>
This keeps components clean and logic focused.
Use watch() and watchEffect() Appropriately
watch() is used when you need to react to changes in a specific reactive source and have access to both old and new values.
import { watch } from 'vue'
watch(count, (newVal, oldVal) => {
console.log(Count changed from ${oldVal} to ${newVal})
})
watchEffect() automatically tracks dependencies and runs immediately useful for side effects like API calls or DOM updates.
import { watchEffect } from 'vue'
watchEffect(() => {
if (user.value) {
fetchUserDetails(user.value.id)
}
})
Use watchEffect() for automatic dependency tracking and watch() for explicit control.
Always Clean Up Side Effects
When using watchEffect(), onMounted(), or async operations, ensure you clean up resources to prevent memory leaks.
onMounted(() => {
const interval = setInterval(() => {
console.log('Running...')
}, 1000)
// Cleanup function
return () => {
clearInterval(interval)
}
})
Returning a cleanup function from onMounted(), onBeforeUnmount(), or watchEffect() ensures resources are properly released when the component unmounts.
Tools and Resources
Official Vue Documentation
The Vue 3 Official Documentation is the most authoritative source for learning the Composition API. It includes interactive examples, TypeScript guides, and API references.
Vue DevTools
The Vue DevTools browser extension is indispensable for debugging Composition API components. It allows you to inspect reactive state, computed properties, and custom composables in real time.
VS Code Extensions
- Volar The official Vue 3 language server for VS Code. Provides syntax highlighting, IntelliSense, type checking, and template interpolation for
<script setup>. - Vetur Legacy extension; avoid for Vue 3 projects. Volar has replaced it.
TypeScript Support
Use tsconfig.json with Vues recommended settings:
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"moduleResolution": "node",
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"types": ["vite/client", "vue"]
},
"include": ["src//*.ts", "src//*.vue", "src/main.ts"]
}
Code Snippets and Templates
Install the Vue 3 Snippets extension in VS Code to quickly generate boilerplate for:
script-setuprefreactivecomputedwatchdefinePropsdefineEmits
Learning Platforms
- Vue Mastery Offers in-depth courses on Composition API and Vue 3.
- Frontend Masters Advanced Vue 3 and TypeScript courses.
- YouTube Channels like Academind and The Net Ninja provide free, high-quality tutorials.
Community and Support
- Vue Forum https://forum.vuejs.org
- Stack Overflow Tag questions with
[vue.js]and[composition-api] - GitHub Discussions Vue 3 repository: https://github.com/vuejs/core/discussions
Real Examples
Example 1: Todo List with Local Storage
A fully functional todo list that persists items in localStorage using a custom composable.
// composables/useTodos.js
import { ref, computed } from 'vue'
export function useTodos() {
const todos = ref(JSON.parse(localStorage.getItem('todos') || '[]'))
const addTodo = (text) => {
if (text.trim()) {
todos.value.push({
id: Date.now(),
text: text.trim(),
completed: false
})
saveTodos()
}
}
const toggleTodo = (id) => {
const todo = todos.value.find(t => t.id === id)
if (todo) todo.completed = !todo.completed
saveTodos()
}
const removeTodo = (id) => {
todos.value = todos.value.filter(t => t.id !== id)
saveTodos()
}
const completedCount = computed(() =>
todos.value.filter(t => t.completed).length
)
const saveTodos = () => {
localStorage.setItem('todos', JSON.stringify(todos.value))
}
return {
todos,
addTodo,
toggleTodo,
removeTodo,
completedCount
}
}
<script setup>
import { useTodos } from '@/composables/useTodos'
const {
todos,
addTodo,
toggleTodo,
removeTodo,
completedCount
} = useTodos()
const newTodo = ref('')
const handleSubmit = (e) => {
e.preventDefault()
addTodo(newTodo.value)
newTodo.value = ''
}
</script>
<template>
<div>
<h2>Todo List</h2>
<form @submit="handleSubmit">
<input v-model="newTodo" placeholder="Add a new todo" />
<button type="submit">Add</button>
</form>
<p>Completed: {{ completedCount }} / {{ todos.length }}</p>
<ul>
<li v-for="todo in todos" :key="todo.id">
<span :class="{ completed: todo.completed }" @click="toggleTodo(todo.id)">
{{ todo.text }}
</span>
<button @click="removeTodo(todo.id)">Remove</button>
</li>
</ul>
</div>
</template>
<style>
.completed {
text-decoration: line-through;
color:
888;
}
</style>
Example 2: Real-Time Search with Debounced API Calls
Search component that fetches data from an API with debounced input to reduce network requests.
// composables/useDebounce.js
import { ref, watchEffect } from 'vue'
export function useDebounce(value, delay = 500) {
const debouncedValue = ref(value)
watchEffect(() => {
const handler = setTimeout(() => {
debouncedValue.value = value
}, delay)
return () => clearTimeout(handler)
})
return debouncedValue
}
// composables/useSearch.js
import { ref, computed } from 'vue'
import { useDebounce } from './useDebounce'
export function useSearch(searchTerm, apiEndpoint) {
const data = ref([])
const loading = ref(false)
const error = ref(null)
const debouncedTerm = useDebounce(searchTerm, 600)
const fetchResults = async () => {
loading.value = true
error.value = null
try {
const res = await fetch(${apiEndpoint}?q=${debouncedTerm.value})
if (!res.ok) throw new Error('Network response was not ok')
data.value = await res.json()
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
watchEffect(fetchResults)
return {
data,
loading,
error
}
}
<script setup>
import { ref } from 'vue'
import { useSearch } from '@/composables/useSearch'
const searchTerm = ref('')
const { data, loading, error } = useSearch(searchTerm, 'https://api.example.com/search')
</script>
<template>
<div>
<input v-model="searchTerm" placeholder="Search..." />
<div v-if="loading">Loading...</div>
<div v-if="error">Error: {{ error }}</div>
<ul v-else>
<li v-for="item in data" :key="item.id">{{ item.name }}</li>
</ul>
</div>
</template>
Example 3: Theme Toggle with Provide/Inject
A global theme system where any component can access and toggle the theme.
// App.vue
<script setup>
import { provide, ref } from 'vue'
const theme = ref('light')
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
document.documentElement.classList.toggle('dark', theme.value === 'dark')
}
provide('theme', theme)
provide('toggleTheme', toggleTheme)
</script>
<template>
<div :class="theme">
<Header />
<Main />
<Footer />
</div>
</template>
// Header.vue
<script setup>
import { inject } from 'vue'
const theme = inject('theme')
const toggleTheme = inject('toggleTheme')
</script>
<template>
<header>
<h1>My App</h1>
<button @click="toggleTheme">Toggle {{ theme }} Mode</button>
</header>
</template>
FAQs
Is the Composition API better than the Options API?
The Composition API is not inherently better its a different approach. For small components with simple logic, the Options API is perfectly fine. However, for large, complex components with shared logic, the Composition API provides superior code organization, reusability, and maintainability.
Can I use the Composition API in Vue 2?
Technically yes through the @vue/composition-api plugin. However, this plugin is deprecated. Vue 2 reached end-of-life in December 2023. All new projects should use Vue 3.
Do I have to use <script setup> with the Composition API?
No. You can use the traditional setup() function. However, <script setup> is now the recommended syntax because it reduces boilerplate and improves developer experience.
Can I mix Options API and Composition API in the same component?
Yes. Vue 3 allows both APIs to coexist. However, this is discouraged as it leads to inconsistent codebases. Choose one pattern and stick with it for maintainability.
How do I test components using the Composition API?
Use testing libraries like Vue Test Utils or Vitest. Custom composables can be tested in isolation since theyre just functions. For components, you can render them and assert behavior as usual.
Does the Composition API affect performance?
No in fact, it often improves performance. The Composition API enables better tree-shaking, reduces component size, and allows for more efficient reactivity tracking. The overhead of ref() and reactive() is minimal and optimized in Vue 3s runtime.
Whats the difference between ref() and reactive()?
ref() creates a reactive reference to a value (primitive or object) and requires .value to access or modify. reactive() creates a reactive object where properties are directly accessible without .value. Use ref() for primitives and reactive() for objects.
Conclusion
The Composition API represents a paradigm shift in how Vue developers structure and manage component logic. By organizing code around concerns rather than options, it empowers teams to build scalable, maintainable, and reusable applications. With features like custom composables, type-safe props and emits, and seamless integration with TypeScript, the Composition API is not just an enhancement its the future of Vue development.
As you begin adopting the Composition API, start small: refactor one component at a time. Focus on extracting reusable logic into composables. Leverage TypeScript for type safety. And always prioritize clean, readable code over clever patterns.
Mastering the Composition API will not only make you a more effective Vue developer it will fundamentally change how you think about state, side effects, and component architecture. The tools are here. The best practices are established. Now its time to build something great.