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.

Vue.jsbeginner
12 min read

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:

  1. Scattered logic. A "search box" feature involved data(), computed, watch, and methods blocks all touching the same concept from different parts of the file.
  2. Hard reuse. Sharing logic between components needed mixins (which obscured where things came from) or scoped slots.
  3. 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.

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

index.jsindex.js
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.

index.jsindex.js
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.

index.jsindex.js
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.

index.jsindex.js
// 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:

vuevue
<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 .value in script. The lint rule vue/no-ref-as-operand catches the worst cases.
  • Destructuring a reactive object and losing reactivity. Use toRefs(form) if you must destructure.
  • Putting expensive work directly in the script body. Wrap it in computed, watchEffect, or onMounted.
  • Using watch for a value you should be computed-ing. If the new state is purely a function of old state, computed is 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 .value
  • computed(() => derived) → cached, read-only
  • watch(src, (n, o) => {...}) → side effect when source changes
  • watchEffect(() => {...}) → runs immediately + on any reactive change inside it
  • Composable = useThing() function returning refs + methods
  • Lifecycle: onMounted, onUnmounted etc., imported from vue
  • Ready-made composables: VueUse
Rune AI

Rune AI

Key Insights

  • The Composition API replaces the Options API for all new Vue 3 code in 2026.
  • ref is the default reactive container; access the value with .value in script.
  • reactive is for plain objects when .value clutters the code; do not destructure naively.
  • computed caches derived values; watch runs side effects when state changes.
  • Composables (useThing()) turn reactive logic into reusable functions you can share across any component.
RunePowered by Rune AI

Frequently Asked Questions

`ref` or `reactive`?

Default to `ref`. It works for any value (primitives, objects, arrays). Use `reactive` for grouped object state where the `.value` ceremony adds noise.

Why does my destructuring break reactivity?

reactive proxy tracks property *access* on the original object. Destructuring copies the current value out, severing the connection. Use `toRefs(obj)` to destructure into individual refs.

Composition API vs React Hooks?

Same idea (functions over classes), different reactivity model. Vue tracks dependencies automatically so you do not write dependency arrays. React's model gives you stricter purity guarantees.

Do I still need Vuex?

No. [Pinia](https://pinia.vuejs.org/) is the official store now and it is just composables under the hood — same APIs you already know.

Can I mix Options and Composition API?

Yes, technically. In practice, pick Composition API for new components and only touch Options API in legacy code.

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.