Since the release of Vue 3, the Composition API has emerged as a powerful alternative to the classic Options API, offering greater flexibility, improved logic reuse, and enhanced TypeScript support. Whether you're building complex enterprise applications or modular front-end components, the Composition API helps you write cleaner and more maintainable code.
In this comprehensive guide, you’ll learn how to master the Vue 3 Composition API from the ground up. We'll cover the core concepts such as setup()
, reactive state, lifecycle hooks, watchers, and creating reusable composables. Each section will include real-world examples to help you understand how to use the Composition API effectively in your own Vue projects.
By the end of this tutorial, you’ll be equipped with the knowledge and practical skills to build scalable Vue 3 applications using the Composition API confidently
Setting Up the Vue 3 Project
To get started with the Composition API, we'll first create a new Vue 3 project using Vite, a fast, modern build tool that is now the recommended way to scaffold Vue applications.
Step 1: Create a New Vue 3 Project
Open your terminal and run the following command:
npm create vite@latest vue3-composition-api -- --template vue
Or if you prefer using yarn
:
yarn create vite vue3-composition-api --template vue
You’ll be prompted to select a project name and template — make sure to choose Vue (not Vue + TypeScript for now, unless you're planning to include TypeScript in this guide).
Step 2: Install Project Dependencies
Navigate into the project directory and install dependencies:
cd vue3-composition-api
npm install
Or with yarn:
yarn install
Step 3: Run the Development Server
Start the development server:
npm run dev
Once the server starts, open your browser and go to http://localhost:5173
. You should see the default Vue 3 welcome page.
Step 4: Clean Up the Project
Let’s clean up the default template so we can start fresh.
-
Open
src/App.vue
and replace it with the following:<template> <h1>Vue 3 Composition API Playground</h1> </template> <script setup> // Nothing here yet — we’ll add Composition API logic soon! </script> <style scoped> h1 { text-align: center; margin-top: 2rem; font-size: 2rem; } </style>
Now that we have a clean Vue 3 project ready, we can dive into the fundamentals of the Composition API, starting with the setup()
function and reactivity.
Understanding setup()
and Basic Reactivity
The setup()
function is the entry point to the Composition API. It replaces the data
, methods
, computed
, and lifecycle
options in the Options API, providing a single place to declare reactive state and logic.
What is setup()
?
The setup()
function runs before the component is created and is called only once. It's where you define and return everything your component needs: reactive state, functions, computed properties, and more.
Let’s start with a simple example of reactivity using ref()
and reactive()
.
Example: Counter with ref()
Update App.vue
with the following:
<template>
<div class="container">
<h1>Counter Example</h1>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>
<style scoped>
.container {
text-align: center;
margin-top: 3rem;
}
button {
padding: 0.5rem 1rem;
font-size: 1rem;
margin-top: 1rem;
}
</style>
Explanation
-
ref(0)
creates a reactive primitive (like a number, string, or boolean). It returns a Ref object. -
You access the actual value via
.value
inside JavaScript. -
Vue automatically unwraps
.value
in the template — you can just use{{ count }}
.
Example: Reactive Object with reactive()
Now let’s see how to use reactive()
for an object.
<script setup>
import { reactive } from 'vue'
const state = reactive({
name: 'Djamware',
age: 8
})
function increaseAge() {
state.age++
}
</script>
<template>
<div class="container">
<h2>{{ state.name }} - Age: {{ state.age }}</h2>
<button @click="increaseAge">Increase Age</button>
</div>
</template>
Key Differences
Feature | ref() |
reactive() |
---|---|---|
Use for | Primitive values | Objects and arrays |
Access | .value |
Direct property access |
Template | No .value needed |
No .value needed |
In the next section, we'll dive deeper into computed properties and watchers, two powerful features that let you react to changes in your reactive data.
Computed Properties and Watchers in the Composition API
Vue’s Composition API gives you access to two powerful tools to work with reactive state: computed properties for deriving values, and watchers for reacting to changes.
Computed Properties with computed()
computed()
is used to create derived reactive values, like a formula that automatically updates when its dependencies change.
Example: Full Name Computation
Update your App.vue
to include:
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
// Computed full name
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`
})
</script>
<template>
<div class="container">
<h2>Full Name: {{ fullName }}</h2>
<input v-model="firstName" placeholder="First Name" />
<input v-model="lastName" placeholder="Last Name" />
</div>
</template>
<style scoped>
input {
margin: 0.5rem;
padding: 0.5rem;
font-size: 1rem;
}
</style>
Why use computed()
?
-
It’s cached: only recalculates when its dependencies change.
-
Great for UI display logic and derived state.
Watchers with watch()
and watchEffect()
Use watchers when you want to run side effects or logic whenever a reactive value changes.
Example: Watching a Value
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
watch(count, (newVal, oldVal) => {
console.log(`Count changed from ${oldVal} to ${newVal}`)
})
</script>
Example: watchEffect()
watchEffect()
runs immediately and re-runs whenever any reactive value inside it changes.
<script setup>
import { ref, watchEffect } from 'vue'
const city = ref('Jakarta')
watchEffect(() => {
console.log(`The selected city is: ${city.value}`)
})
</script>
When to Use Each
Tool | Use When |
---|---|
computed() |
You need a derived value based on other reactive state. |
watch() |
You want to perform a specific action when a particular value changes. |
watchEffect() |
You want automatic tracking of all used reactive dependencies. |
Up next: we’ll explore Vue 3 Lifecycle Hooks in the Composition API, such as onMounted
, onUpdated
, and onUnmounted
.
Using Lifecycle Hooks with the Composition API
In Vue 2's Options API, lifecycle hooks like mounted
, created
, and destroyed
are defined as top-level options in the component. In the Composition API, Vue provides composable lifecycle functions that you can call inside the setup()
function.
These include:
-
onMounted()
-
onUnmounted()
-
onUpdated()
-
onBeforeMount()
,onBeforeUpdate()
,onBeforeUnmount()
, etc.
Example: Using onMounted()
Let’s simulate an API call or DOM interaction when the component mounts.
<script setup>
import { ref, onMounted } from 'vue'
const message = ref('Loading...')
onMounted(() => {
setTimeout(() => {
message.value = 'Data loaded successfully!'
}, 1500)
})
</script>
<template>
<div class="container">
<h2>{{ message }}</h2>
</div>
</template>
Explanation:
-
onMounted()
is called once after the component is added to the DOM. -
You can perform data fetching, DOM access, or subscriptions here.
Example: onUnmounted()
for Cleanup
<script setup>
import { onUnmounted } from 'vue'
let intervalId
intervalId = setInterval(() => {
console.log('Running some interval...')
}, 1000)
onUnmounted(() => {
clearInterval(intervalId)
console.log('Component is unmounted. Interval cleared.')
})
</script>
Best Use Case:
Use onUnmounted()
to clean up resources like intervals, event listeners, or WebSocket connections.
Full List of Common Lifecycle Hooks
Hook | When It Runs |
---|---|
onBeforeMount |
Right before the component is mounted |
onMounted |
After the component is mounted |
onBeforeUpdate |
Before reactive data causes a DOM update |
onUpdated |
After a DOM update |
onBeforeUnmount |
Right before the component is unmounted |
onUnmounted |
After the component is removed from the DOM |
All of these hooks must be called synchronously inside setup()
.
Now that you're familiar with reactivity, computed properties, watchers, and lifecycle hooks, the next step is one of the biggest benefits of the Composition API: creating reusable logic through composables.
Building Custom Composables
A composable is simply a function that uses Composition API features (ref
, reactive
, computed
, watch
, etc.) to encapsulate and reuse logic across multiple components.
This is similar to React Hooks and is perfect for organizing business logic, separating concerns, and reducing duplication.
Example: A useCounter()
Composable
Let’s extract the counter logic into a reusable composable.
Step 1: Create a New Composable File
Create a new file in your src
directory:
src/composables/useCounter.js
// src/composables/useCounter.js
import { ref } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const increment = () => count.value++
const decrement = () => count.value--
const reset = () => count.value = initialValue
return {
count,
increment,
decrement,
reset
}
}
Step 2: Use the Composable in a Component
<script setup>
import { useCounter } from './composables/useCounter'
const { count, increment, decrement, reset } = useCounter(5)
</script>
<template>
<div class="container">
<h2>Count: {{ count }}</h2>
<button @click="increment">+</button>
<button @click="decrement">-</button>
<button @click="reset">Reset</button>
</div>
</template>
<style scoped>
button {
margin: 0.25rem;
padding: 0.5rem 1rem;
}
</style>
Benefits of Using Composables
✅ Encapsulation: Keep logic self-contained and modular
✅ Reusability: Use the same composable across multiple components
✅ Testability: Pure functions are easier to test
✅ Maintainability: Cleaner and more organized codebase
Tip: Organize Composables in a Folder
You can store all your custom composables in src/composables/
, following a naming convention like useXyz.js
or useXyz.ts
.
Some common examples:
-
useFetch()
– encapsulate API fetching logic -
useForm()
– handle form state and validation -
useDarkMode()
– toggle dark/light themes -
useLocalStorage()
– sync state with localStorage
In the next section, we’ll build a real-world Composition API example using everything you’ve learned so far.
Real-World Example: Todo List App with Composition API
We’ll build a small but complete to-do list app using ref
, computed
, watch
, a custom composable, and lifecycle hooks — all the core Composition API concepts.
Step 1: Create the useTodos
Composable
Create a new file:
src/composables/useTodos.js
import { ref, computed } from 'vue'
export function useTodos() {
const todos = ref([])
const newTodo = ref('')
const addTodo = () => {
if (newTodo.value.trim()) {
todos.value.push({
id: Date.now(),
text: newTodo.value.trim(),
done: false
})
newTodo.value = ''
}
}
const toggleTodo = (id) => {
const todo = todos.value.find(t => t.id === id)
if (todo) todo.done = !todo.done
}
const removeTodo = (id) => {
todos.value = todos.value.filter(t => t.id !== id)
}
const remaining = computed(() =>
todos.value.filter(t => !t.done).length
)
return {
todos,
newTodo,
addTodo,
toggleTodo,
removeTodo,
remaining
}
}
Step 2: Use the Composable in App.vue
<script setup>
import { useTodos } from './composables/useTodos'
const {
todos,
newTodo,
addTodo,
toggleTodo,
removeTodo,
remaining
} = useTodos()
</script>
<template>
<div class="container">
<h1>Vue 3 Composition API Todo List</h1>
<form @submit.prevent="addTodo">
<input v-model="newTodo" placeholder="Enter a todo..." />
<button type="submit">Add</button>
</form>
<ul>
<li
v-for="todo in todos"
:key="todo.id"
:class="{ done: todo.done }"
@click="toggleTodo(todo.id)"
>
{{ todo.text }}
<button @click.stop="removeTodo(todo.id)">×</button>
</li>
</ul>
<p>{{ remaining }} todo(s) remaining</p>
</div>
</template>
<style scoped>
.container {
max-width: 400px;
margin: 2rem auto;
text-align: center;
}
input {
padding: 0.5rem;
width: 70%;
}
button {
padding: 0.5rem;
margin-left: 0.5rem;
}
ul {
list-style: none;
padding: 0;
}
li {
cursor: pointer;
margin: 0.5rem 0;
padding: 0.5rem;
background: #f9f9f9;
border-radius: 4px;
}
li.done {
text-decoration: line-through;
color: #aaa;
}
li button {
float: right;
border: none;
background: transparent;
font-size: 1.2rem;
color: #c00;
cursor: pointer;
}
</style>
Features Demonstrated:
-
✅ Reactive state with
ref
-
✅ Derived state with
computed
-
✅ Event handlers with
setup()
-
✅ DOM interaction (via user events)
-
✅ Modular logic with custom composables
Up next: we’ll wrap up this guide with a conclusion and some best practices for using the Vue 3 Composition API effectively.
Conclusion and Best Practices
The Composition API in Vue 3 offers a more flexible, scalable, and modular way to build modern web applications. By shifting from the traditional Options API to the Composition API, you gain better code organization, enhanced logic reuse, and a smoother path to TypeScript adoption.
In this guide, you’ve learned:
✅ How to set up a Vue 3 project with Vite
✅ The role of the setup()
function and how reactivity works with ref
and reactive
✅ How to create a derived state with computed
and respond to changes with watch
✅ How to use lifecycle hooks like onMounted
and onUnmounted
✅ How to encapsulate logic into reusable composables
✅ How to build a real-world Todo app using all of the above
Composition API Best Practices
Here are some tips to make the most of the Composition API in your projects:
-
Use composables for logic reuse
Extract business logic intouseXyz
functions — it keeps components lean and promotes modularity. -
Organize by feature, not by type
Group files by feature (e.g., todos, auth, etc.), not by file type (components, stores, utils). This scales better. -
Prefer
ref
for primitives,reactive
for objects
Useref
for strings, numbers, booleans;reactive
for objects and arrays — and avoid mixing them unnecessarily. -
Keep
setup()
clean
Don’t cram too much logic into a singlesetup()
— compose it from smaller composables. -
Name your composables clearly
Stick withuseSomething()
naming — it aligns with community conventions and helps with auto-imports in some IDEs. -
Start small, adopt gradually
You don’t have to refactor everything at once — Vue 3 supports both Options and Composition APIs side-by-side.
What’s Next?
Here are a few directions you could explore after mastering the basics:
-
TypeScript with the Composition API
-
Vue Router and Navigation Guards in
setup()
-
Global state management with Pinia (Composition API-friendly Vuex alternative)
-
Server-side rendering with Nuxt 3
-
Testing composables with Vitest
You can get the full source code on our GitHub.
===That's just the basics. If you need more deep learning about Vue, you can take the following cheap course:
-
Vuex Vuex with Vue Js Projects to Build Web Application UI
-
Vue 3 and Deno: A Practical Guide
-
Vue 3 Fundamentals Beginners Guide 2023
-
Vue 3, Nuxt. js and NestJS: A Rapid Guide - Advanced
-
Master Vuejs from scratch (incl Vuex, Vue Router)
-
Laravel 11 + Vue 3 + TailwindCSS: Fullstack personal blog.
Thanks!