Mastering Vue 3 Composition API: A Complete Guide

by Didin J. on Jul 01, 2025 Mastering Vue 3 Composition API: A Complete Guide

Master Vue 3 Composition API with this complete guide covering setup, reactivity, lifecycle hooks, composables, and a real-world todo list example.

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.

Mastering Vue 3 Composition API: A Complete Guide - vue vite home

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:

  1. Use composables for logic reuse
    Extract business logic into useXyz functions — it keeps components lean and promotes modularity.

  2. 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.

  3. Prefer ref for primitives, reactive for objects
    Use ref for strings, numbers, booleans; reactive for objects and arrays — and avoid mixing them unnecessarily.

  4. Keep setup() clean
    Don’t cram too much logic into a single setup() — compose it from smaller composables.

  5. Name your composables clearly
    Stick with useSomething() naming — it aligns with community conventions and helps with auto-imports in some IDEs.

  6. 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:

Thanks!