The Vue Composition API is the modern authoring style introduced in Vue 3 that lets you gather a component's logic inside a single setup function instead of spreading it across options like data, methods and computed. In this guide I walk through the core tools — ref, reactive, computed and watch — with real examples, so you can keep growing components tidy and reusable.
From the Options API to the Composition API
In the classic Options API a component's state, computed values and methods live in separate blocks. That's fine for small components, but as a feature grows, code that belongs to the same concern ends up scattered across the file. The Composition API lets you keep related logic together: a counter's state, its derived value and its function all sit side by side.
Both styles are fully supported in Vue 3 — the Composition API is an option, not a requirement. Its biggest payoff shows up when you write shareable logic (composables).
setup and <script setup>
The most practical way to use the Composition API is the compile-time <script setup> syntax. Every variable and function you declare there is automatically exposed to the template, and you don't need a return statement.
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>
<template>
<button @click="increment">Count: {{ count }}</button>
</template>
Note: inside JavaScript you read the value through count.value, but in the template Vue unwraps .value automatically, so you just write count.
Reactive values with ref
Use ref for single values (number, string, boolean). It returns an object whose actual value lives in its .value property. This is the basis of the reactivity that lets Vue update the DOM when the value changes.
- Read/write:
const name = ref('Ada')→name.value = 'Grace' - Ideal for primitives: string, number, boolean.
- Can hold objects too: put an object in a
refand Vue makes its contents deeply reactive withreactive.
Object state with reactive
When you want to manage several related fields together, reactive is a good fit. It takes an object and turns it into a deeply reactive proxy; here you don't write .value, you access properties directly.
import { reactive } from 'vue'
const form = reactive({
email: '',
agreed: false,
})
function submit() {
console.log(form.email, form.agreed)
}
Which to pick? A practical rule: use ref for primitives and individual pieces of state, and reactive for a form or object that logically forms a whole. Remember you can't reassign a reactive object wholesale (you'd break the reference) — update its fields instead, or prefer ref.
Derived values with computed
computed produces values calculated from other reactive sources, cached until their dependencies change. Instead of recomputing the same result repeatedly in the template, you define it once.
import { ref, computed } from 'vue'
const price = ref(100)
const quantity = ref(3)
const total = computed(() => price.value * quantity.value)
total behaves like a ref: total.value in JavaScript, {{ total }} in the template. When price or quantity changes, Vue recomputes total automatically.
Side effects with watch and watchEffect
For side effects — firing a network request, writing to localStorage — when a value changes, you reach for watch. It watches a specific source and gives you the old and new values.
import { ref, watch } from 'vue'
const query = ref('')
watch(query, (next, prev) => {
console.log(`'${prev}' -> '${next}'`)
})
watchEffect automatically tracks the reactive values used inside it and runs immediately once; it's shorter for simple cases. Reach for watch when you want to see exactly which dependency you're tracking, and watchEffect when you mean "track everything I use."
Composables: reusing logic
The real power of the Composition API is extracting logic into a function and sharing it across components. By convention these functions are named with a use prefix.
// useCounter.js
import { ref } from 'vue'
export function useCounter(start = 0) {
const count = ref(start)
const increment = () => count.value++
const reset = () => (count.value = start)
return { count, increment, reset }
}
Now any component can reuse the same logic with const { count, increment } = useCounter(10). Unlike mixins, it's clear where each value comes from and you won't run into name collisions.
Frequently Asked Questions
Should I use ref or reactive?
Use ref for primitives and individual state, and reactive for objects/forms that form a logical whole. Many teams use ref everywhere for consistency; both are correct.
Is the Options API now considered legacy?
No. The Options API is still fully supported and isn't being removed. The Composition API is an additional option that shines in large components and shared logic.
Why don't I write .value in the template?
When Vue compiles the template it automatically unwraps top-level refs. That's why you write count in the template but count.value in JavaScript.
Thinking about moving your Vue project to the Composition API? From component architecture to composable design, we can build a clean structure together — get in touch.