Skip to main content
Vue.js for Beginners to Advanced
CHAPTER 20 Beginner

Vue Composition API

Updated: May 18, 2026
5 min read

# CHAPTER 20

Vue Composition API

1. Chapter Introduction

The Composition API is Vue 3's most significant architectural advancement. Instead of organizing code by OPTIONS (data, methods, computed, watch), you organize by FEATURE — all the logic for one feature lives together. This makes large components readable and logic reusable via composables.

2. Learning Objectives

  • Understand why the Composition API exists.
  • Use <script setup> syntax.
  • Build reusable composables.
  • Compare Options API vs Composition API.
  • Use defineExpose, defineProps, defineEmits macros.

3. Options API vs Composition API

text
12345678910111213141516171819202122
Options API (Vue 2 style):          Composition API (Vue 3):

export default {                    <script setup>
  data() {                          // All feature code together
    return {                        const count = ref(0)
      count: 0,    ← scattered     const doubled = computed(...)
      name: &#039;'     ← feature A     function increment() {...}
    }                               </script>
  },
  computed: {
    doubled() {...}  ← scattered
    // also feature B code here
  },
  methods: {
    increment() {...} ← scattered
  }
}

Problem: In a 500-line component, all "counter" logic
is spread across data, computed, methods, watch.

Solution: Composition API groups all counter logic together.

4. <script setup> Deep Dive

vue
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
<script setup>
// ✅ This is the MODERN way to write Vue 3 components
// Everything declared here is available in the template automatically

import { ref, reactive, computed, watch, onMounted } from &#039;vue'
import { useRoute } from &#039;vue-router'
import { useUserStore } from &#039;@/stores/user'

// Props — no need to return
const props = defineProps({
  title: String,
  initialCount: { type: Number, default: 0 }
})

// Emits
const emit = defineEmits([&#039;update', 'close'])

// Reactive state
const count = ref(props.initialCount)
const user = reactive({ name: &#039;Alice', role: 'admin' })

// Computed
const doubled = computed(() => count.value * 2)

// Store access
const userStore = useUserStore()

// Router
const route = useRoute()

// Methods
function increment() {
  count.value++
  emit(&#039;update', count.value)
}

// Lifecycle
onMounted(() => {
  console.log(&#039;Mounted with count:', count.value)
})

// Watch
watch(count, (newVal) => {
  if (newVal > 10) emit(&#039;close')
})

// Expose to parent (via template ref)
// By default, <script setup> is closed — nothing is exposed
defineExpose({ count, increment })
</script>

<template>
  <div>
    <h2>{{ title }}</h2>
    <p>Count: {{ count }} (doubled: {{ doubled }})</p>
    <button @click="increment">+</button>
  </div>
</template>

5. Composables — Reusable Logic

javascript
1234567891011121314151617181920212223242526
// src/composables/useFetch.js
import { ref } from &#039;vue&#039;

export function useFetch(url) {
  const data = ref(null)
  const loading = ref(false)
  const error = ref(null)

  async function fetchData() {
    loading.value = true
    error.value = null
    try {
      const res = await fetch(url)
      if (!res.ok) throw new Error(`HTTP ${res.status}`)
      data.value = await res.json()
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }

  fetchData()

  return { data, loading, error, refetch: fetchData }
}
javascript
12345678910111213
// src/composables/useLocalStorage.js
import { ref, watch } from &#039;vue&#039;

export function useLocalStorage(key, defaultValue) {
  const stored = localStorage.getItem(key)
  const value = ref(stored ? JSON.parse(stored) : defaultValue)

  watch(value, (newVal) => {
    localStorage.setItem(key, JSON.stringify(newVal))
  }, { deep: true })

  return value
}
javascript
12345678910111213141516
// src/composables/useDebounce.js
import { ref, watch } from &#039;vue&#039;

export function useDebounce(value, delay = 300) {
  const debouncedValue = ref(value.value)
  let timeout

  watch(value, (newVal) => {
    clearTimeout(timeout)
    timeout = setTimeout(() => {
      debouncedValue.value = newVal
    }, delay)
  })

  return debouncedValue
}
javascript
12345678910111213141516
// src/composables/useCounter.js
import { ref, computed } from &#039;vue&#039;

export function useCounter(initial = 0, options = {}) {
  const { min = -Infinity, max = Infinity, step = 1 } = options
  const count = ref(initial)
  const isMin = computed(() => count.value <= min)
  const isMax = computed(() => count.value >= max)

  function increment() { if (!isMax.value) count.value += step }
  function decrement() { if (!isMin.value) count.value -= step }
  function reset() { count.value = initial }
  function set(val) { count.value = Math.max(min, Math.min(max, val)) }

  return { count, isMin, isMax, increment, decrement, reset, set }
}
javascript
1234567891011121314151617
// src/composables/useMousePosition.js
import { ref, onMounted, onUnmounted } from &#039;vue&#039;

export function useMousePosition() {
  const x = ref(0)
  const y = ref(0)

  function update(e) {
    x.value = e.clientX
    y.value = e.clientY
  }

  onMounted(() => window.addEventListener(&#039;mousemove&#039;, update))
  onUnmounted(() => window.removeEventListener(&#039;mousemove&#039;, update))

  return { x, y }
}

6. Using Composables in Components

vue
1234567891011121314151617181920212223242526
<script setup>
// Clean component — all logic extracted to composables
import { useFetch } from &#039;@/composables/useFetch'
import { useLocalStorage } from &#039;@/composables/useLocalStorage'
import { useCounter } from &#039;@/composables/useCounter'
import { useMousePosition } from &#039;@/composables/useMousePosition'

// Reuse logic anywhere with zero code duplication
const { data: posts, loading, error } = useFetch(&#039;https://jsonplaceholder.typicode.com/posts?_limit=5')
const theme = useLocalStorage(&#039;theme', 'light')
const { count, increment, decrement } = useCounter(0, { min: 0, max: 10 })
const { x, y } = useMousePosition()
</script>

<template>
  <div :data-theme="theme">
    <button @click="theme = theme === &#039;light' ? 'dark' : 'light'">Toggle Theme</button>
    <p>Mouse: {{ x }}, {{ y }}</p>
    <p>Count: {{ count }}</p>
    <button @click="decrement">-</button>
    <button @click="increment">+</button>
    <ul v-if="!loading">
      <li v-for="post in posts" :key="post.id">{{ post.title }}</li>
    </ul>
  </div>
</template>

7. Common Mistakes

  • Calling composables conditionally: Composables must be called at the top level of setup(), never inside if, for, or event handlers — they track lifecycle.
  • Not returning from composables: A composable that doesn't return anything is useless. Always return the reactive state and methods the consumer needs.

8. MCQs

Question 1

Composables solve?

Question 2

<script setup> requires explicit return?

Question 3

Composable naming convention?

Question 4

defineExpose() is for?

Question 5

Can you call composables conditionally?

Question 6

Options API equivalent of composables?

Question 7

watch in a composable with onUnmounted cleanup?

Question 8

defineProps in <script setup> is a?

Question 9

Best place to put reusable composable files?

Question 10

Composition API advantage over Options API?

9. Interview Questions

  • Q: What is a Vue composable and how does it differ from a mixin?
  • Q: Explain the advantages of the Composition API over the Options API.

10. Summary

The Composition API's <script setup> syntax is the modern standard for Vue 3 development. Composables (functions starting with use) extract and share stateful logic between components — replacing the error-prone mixin pattern. This is what makes Vue 3 scalable for large applications.

11. Next Chapter Recommendation

In Chapter 21: Vue Transitions and Animations, we add motion to Vue applications using the <Transition> component and CSS animations.

Finish this Chapter

Save your progress on your learning path and prepare for coding interview challenges.

Discussion

Join the discussion

Log in or create a free account to participate.

Sort: ·