Vue Composition API Explained for Beginners with Real Examples
A friendly walkthrough of the Vue Composition API. Learn what ref, reactive, computed, and watch do, when to use each, and how composables make your Vue code reusable across components in 2026.
The Composition API is what makes Vue feel modern. Before Vue 3, components were objects with data(), computed, methods, and watch sections (the Options API). It worked, but related logic ended up scattered across different sections, and reusing logic across components meant mixins or render-prop gymnastics. The Composition API replaces that with plain JavaScript functions you call from <script setup> — and once you see it, the old style feels like a quaint detour.
This guide walks through the four hooks you will use 95% of the time — ref, reactive, computed, watch — and the one pattern (composables) that turns them into reusable building blocks. By the end you will read any modern Vue codebase comfortably.
Why the Composition API Exists
Three problems with the Options API stacked up over the years:
- Scattered logic. A "search box" feature involved
data(),computed,watch, andmethodsblocks all touching the same concept from different parts of the file. - Hard reuse. Sharing logic between components needed mixins (which obscured where things came from) or scoped slots.
- Weak TypeScript. The Options API's
this-based shape was painful to type.
The Composition API, paired with <script setup>, fixes all three. Logic for one feature lives in one block. Reusable logic becomes a function (a composable) you can import. TypeScript inference is essentially perfect.
ref: The Default Way to Hold Reactive State
ref() wraps any value in a reactive container. You read and write it through .value in script, and Vue auto-unwraps it in templates.
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() { count.value++ }
</script>
<template>
<button @click="increment">Clicked {{ count }} times</button>
</template>Use ref for anything: numbers, strings, booleans, arrays, objects, even DOM nodes (const el = ref(null) then <div ref="el" />). It is the default; reach for it first and only consider alternatives when you have a reason.
Two beginner footguns: forgetting .value in script (you get the ref object, not the value), and trying to destructure a ref (const { value } = count breaks reactivity — use count.value directly or toRef/toRefs).
reactive: For Plain Objects When .value Annoys You
reactive() makes an object deeply reactive without the .value ceremony.
import { reactive } from 'vue'
const form = reactive({ email: '', password: '', remember: false })
// in template: v-model="form.email"Use reactive for grouped form state, store-like objects, and any time you find yourself writing .value ten times in a row. The trade-off: you lose reactivity if you destructure (const { email } = form is a one-time read), and you cannot reassign the whole thing (form = newForm breaks it). For most cases, ref is safer; reach for reactive when the object grouping is more useful than the .value shorthand.
computed: Derived Values That Cache Themselves
When a value can be derived from other reactive state, wrap that derivation in computed() and Vue caches it until a dependency changes.
import { ref, computed } from 'vue'
const items = ref([{ qty: 2, price: 5 }, { qty: 1, price: 12 }])
const total = computed(() => items.value.reduce((s, i) => s + i.qty * i.price, 0))total.value reads exactly like a ref but is read-only by default. Vue tracks which refs you used inside the function and only recomputes when one of them changes — so {{ total }} rendered ten times calls the function once.
Use computed for filtered lists, totals, formatted strings, "is this form valid?" booleans — anything you find yourself recomputing inside a template.
watch: Side Effects When State Changes
Sometimes you need to do something when state changes — call an API, write to localStorage, log analytics. That is watch.
import { ref, watch } from 'vue'
const query = ref('')
watch(query, async (newQuery) => {
if (!newQuery) return
const res = await fetch(`/api/search?q=${newQuery}`)
results.value = await res.json()
})watch takes a source (a ref, an array of refs, or a getter function) and a callback that receives (newValue, oldValue). There is also watchEffect, which runs immediately and re-runs whenever any reactive value it touches changes — handy for setup-on-mount patterns.
For derivations, prefer computed. For side effects, use watch or watchEffect. If you find yourself updating other reactive state inside a watch, you usually want computed instead.
Composables: Reusable Logic in One Function
A composable is just a function that uses Vue's reactivity APIs and returns refs and methods. Convention: name it useSomething. This is where the Composition API really pays off.
// composables/useCounter.js
import { ref, computed } from 'vue'
export function useCounter(initial = 0) {
const count = ref(initial)
const isPositive = computed(() => count.value > 0)
return { count, isPositive, inc: () => count.value++ }
}Use it in any component:
<script setup>
import { useCounter } from '@/composables/useCounter'
const { count, isPositive, inc } = useCounter(10)
</script>The same shape covers useFetch, useLocalStorage, useMouse, useDebounce — and the VueUse library ships hundreds of these for free. Once you start writing composables you will rarely write a class or a mixin again.
Common Mistakes Beginners Make
- Forgetting
.valuein script. The lint rulevue/no-ref-as-operandcatches the worst cases. - Destructuring a
reactiveobject and losing reactivity. UsetoRefs(form)if you must destructure. - Putting expensive work directly in the script body. Wrap it in
computed,watchEffect, oronMounted. - Using
watchfor a value you should becomputed-ing. If the new state is purely a function of old state,computedis the right tool. - Mixing Options API and Composition API in the same component for no reason. Pick one (Composition for new code).
Quick Reference
ref(0)→ reactive container; read/write via.value(auto-unwrapped in templates)reactive({...})→ deeply reactive object; no.valuecomputed(() => derived)→ cached, read-onlywatch(src, (n, o) => {...})→ side effect when source changeswatchEffect(() => {...})→ runs immediately + on any reactive change inside it- Composable =
useThing()function returning refs + methods - Lifecycle:
onMounted,onUnmountedetc., imported fromvue - Ready-made composables: VueUse
Rune AI
Key Insights
- The Composition API replaces the Options API for all new Vue 3 code in 2026.
refis the default reactive container; access the value with.valuein script.reactiveis for plain objects when.valueclutters the code; do not destructure naively.computedcaches derived values;watchruns side effects when state changes.- Composables (
useThing()) turn reactive logic into reusable functions you can share across any component.
Frequently Asked Questions
`ref` or `reactive`?
Why does my destructuring break reactivity?
Composition API vs React Hooks?
Do I still need Vuex?
Can I mix Options and Composition API?
Conclusion
ref, reactive, computed, watch, plus composables — that is the entire Composition API surface you need to be productive. Build a small component that uses all four, then extract one piece into a useSomething composable and import it from a second component. That single exercise locks in 90% of the model.