We earn commissions when you shop through the links below.
If you’ve been putting off learning Vue 3, this Vue 3 Composition API tutorial for beginners is the place to start. The Composition API is the modern way to write Vue components — it replaces the old Options API with a more flexible, function-based approach that scales much better as your app grows. In this guide, I’ll walk you through the core concepts with real code examples so you can start writing Composition API components today.
Why the Composition API?
The Options API (the original Vue 2 style) organized component logic into buckets: data, methods, computed, watch. That works fine for small components, but when a component gets complex, related logic ends up scattered across different sections. You’d have a data property, a method, and a watch all dealing with the same feature — but sitting far apart in the file.
The Composition API lets you group related logic together in one place. It’s also much easier to extract and reuse logic across components via composables, which are just plain functions. Once it clicks, you won’t want to go back.
If you want a more structured path through Vue 3 beyond this article, Udemy has several highly-rated Vue 3 courses that go deep on the Composition API with project-based learning.
Setting Up a Vue 3 Project
The fastest way to get started is with Vite:
npm create vite@latest my-vue-app -- --template vue
cd my-vue-app
npm install
npm run devThis gives you a minimal Vue 3 app running locally. Open src/App.vue and you’re ready to follow along.
The setup() Function
Everything in the Composition API lives inside setup(). It’s a special component option that runs before the component is created. Whatever you return from setup() is available in the template.
<template>
<div>
<p>{{ message }}</p>
<button @click="greet">Greet</button>
</div>
</template>
<script>
export default {
setup() {
const message = 'Hello from the Composition API'
function greet() {
alert(message)
}
return { message, greet }
}
}
</script>In practice, most Vue 3 code uses the <script setup> shorthand, which removes the boilerplate of manually returning everything. I’ll use that for the rest of this article.
Reactive State with ref()
ref() is how you create reactive primitive values — strings, numbers, booleans. Access or mutate the value via .value in JavaScript, but Vue unwraps it automatically in the template.
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">+1</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>One common beginner mistake: forgetting .value inside <script setup>. In the template you write {{ count }}, but in your script logic you always need count.value.
Reactive Objects with reactive()
reactive() is better suited for objects. It makes the entire object deeply reactive, and you don’t need .value — you access properties directly.
<template>
<div>
<p>{{ user.name }} — {{ user.age }} years old</p>
<button @click="birthday">Happy Birthday</button>
</div>
</template>
<script setup>
import { reactive } from 'vue'
const user = reactive({
name: 'Alice',
age: 28
})
function birthday() {
user.age++
}
</script>A practical rule of thumb: use ref() for primitives and standalone values, use reactive() for objects with multiple related properties. Some developers use ref() for everything (it works for objects too), which keeps the API consistent at the cost of more .value usage.
Computed Properties
Computed properties work the same as in the Options API — they’re cached based on their reactive dependencies and only re-evaluate when something they depend on changes.
<template>
<div>
<input v-model="firstName" placeholder="First name" />
<input v-model="lastName" placeholder="Last name" />
<p>Full name: {{ fullName }}</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('')
const lastName = ref('')
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`.trim()
})
</script>Watchers with watch() and watchEffect()
When you need to run side effects in response to reactive state changes, use watch() or watchEffect().
watch() is explicit — you tell it what to watch and it gives you both the new and old value:
<script setup>
import { ref, watch } from 'vue'
const searchQuery = ref('')
watch(searchQuery, (newVal, oldVal) => {
console.log(`Search changed from "${oldVal}" to "${newVal}"`)
// trigger API call, etc.
})
</script>watchEffect() is more automatic — it runs immediately and tracks any reactive dependencies it accesses:
<script setup>
import { ref, watchEffect } from 'vue'
const userId = ref(1)
watchEffect(() => {
console.log(`Fetching data for user ${userId.value}`)
// userId.value is tracked automatically
})
</script>Use watch() when you need the previous value or want lazy execution. Use watchEffect() for simpler cases where you just want to react to whatever dependencies your effect uses.
Lifecycle Hooks
Lifecycle hooks are imported functions that you call inside <script setup>:
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const data = ref(null)
onMounted(async () => {
const response = await fetch('https://api.example.com/data')
data.value = await response.json()
})
onUnmounted(() => {
console.log('Component removed from DOM')
})
</script>The most commonly used hooks are onMounted, onUpdated, and onUnmounted. They map directly to the Options API equivalents (mounted, updated, beforeUnmount).
Building a Composable
The real power of the Composition API shows up when you extract reusable logic into composables — plain JavaScript functions that use Vue’s reactivity system. By convention, composable function names start with use.
// src/composables/useCounter.js
import { ref } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
function increment() { count.value++ }
function decrement() { count.value-- }
function reset() { count.value = initialValue }
return { count, increment, decrement, reset }
}Now use it in any component:
<template>
<div>
<p>{{ count }}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
<button @click="reset">Reset</button>
</div>
</template>
<script setup>
import { useCounter } from './composables/useCounter'
const { count, increment, decrement, reset } = useCounter(10)
</script>This pattern replaces mixins entirely — it’s more explicit, easier to test, and there’s no naming collision risk.
Deploying Your Vue App
Once you’re ready to ship, you’ll need somewhere to host it. Railway is a solid option for deploying full-stack Vue apps, especially if your frontend talks to a Node or other backend service. For static Vue builds, Hostinger offers affordable static hosting that works well for Vite-built Vue projects.
What to Learn Next
This Vue 3 Composition API tutorial for beginners covers the fundamentals — ref, reactive, computed, watch, lifecycle hooks, and composables. That’s genuinely enough to build real applications.
From here, the natural next steps are:
- Vue Router 4 — client-side routing with the Composition API
- Pinia — the official state management library, built with the Composition API in mind
- Async composables — handling loading states, errors, and data fetching patterns
- TypeScript + Vue 3 —
<script setup lang="ts">makes typing composables and props straightforward
The Composition API has a small learning curve if you’re coming from the Options API or React hooks, but it’s consistent and composable in ways that pay off quickly. Keep writing components, extract things into composables when logic repeats, and the patterns will become second nature fast.