Vue.js Composition API

The Composition API is a modern way to write Vue components introduced in Vue 3. Instead of organizing code by option type (data, methods, computed), you organize it by feature. All related code — data, logic, and computed values — for a single feature lives together in one place.

Options API vs Composition API

You have used the Options API throughout this course. It groups code into fixed sections: data(), methods, computed, and so on. For small components this works well. For large components with several features, related code ends up scattered across different sections.

Diagram: Options API vs Composition API Code Organization

Options API — organized by option type:
┌─────────────────────────────────────────────┐
│ data()    → userName, searchQuery, cartItems│
│ methods   → fetchUser, searchProducts,      │
│             addToCart, removeFromCart       │
│ computed  → fullName, searchResults,        │
│             cartTotal                       │
└─────────────────────────────────────────────┘
Feature code is split across sections.
To understand "cart", you jump between data, methods, and computed.

Composition API — organized by feature:
┌─────────────────────────────────────────────┐
│ // User feature                             │
│ const userName = ref("")                    │
│ const fullName = computed(...)              │
│ function fetchUser() { ... }                │
│                                             │
│ // Search feature                           │
│ const searchQuery = ref("")                 │
│ const searchResults = computed(...)         │
│ function searchProducts() { ... }           │
│                                             │
│ // Cart feature                             │
│ const cartItems = ref([])                   │
│ const cartTotal = computed(...)             │
│ function addToCart() { ... }                │
└─────────────────────────────────────────────┘
All cart code is together. All search code is together.

The setup() Function

The Composition API lives inside a function called setup(). It runs before the component is created. Everything you return from setup() becomes available in the template.

<template>
  <p>Count: {{ count }}</p>
  <button @click="increment">Add One</button>
</template>

<script>
import { ref } from "vue";

export default {
  setup() {
    const count = ref(0);

    function increment() {
      count.value++;
    }

    return { count, increment };
  }
};
</script>

Diagram: setup() Return Value Becomes Template Data

setup() runs
  │
  ├── creates count (ref)
  ├── creates increment (function)
  │
  └── returns { count, increment }
                │
                ▼
  Template receives count and increment
  {{ count }}         → reads count.value
  @click="increment"  → calls the function

ref — Reactive Single Values

ref() creates a reactive value. You pass the initial value as the argument. Inside setup(), access and change the value through .value. In the template, Vue automatically unwraps .value — you just write {{ count }}.

import { ref } from "vue";

const name    = ref("Alice");       // string
const age     = ref(30);            // number
const active  = ref(true);          // boolean
const items   = ref(["a", "b"]);    // array

// Reading the value in JavaScript:
console.log(name.value);   // "Alice"

// Changing the value:
name.value = "Bob";        // triggers reactivity → template updates

// In the template:
// {{ name }}  → "Bob"   (no .value needed)

Diagram: ref Reactive Wrapper

ref("Alice")
        │
        ▼
  { value: "Alice" }   ← Vue watches this object

name.value = "Bob"
        │
        ▼
  Vue detects change
        │
        ▼
  Template re-renders with "Bob"

reactive — Reactive Objects

reactive() makes an entire object reactive. Unlike ref, you do not need .value — you access properties directly.

import { reactive } from "vue";

const user = reactive({
  name: "Carlos",
  age: 28,
  isAdmin: false
});

// Reading:
console.log(user.name);   // "Carlos"

// Changing:
user.name = "Diana";      // reactive — template updates automatically
user.age++;

// In template:
// {{ user.name }}  → "Diana"
// {{ user.age }}   → 29

When to Use ref vs reactive

┌──────────────────────┬──────────────────────────────────────┐
│ Use ref for          │ Use reactive for                     │
├──────────────────────┼──────────────────────────────────────┤
│ Single values        │ Groups of related data               │
│ (string, number,     │ (form fields, user profile,          │
│  boolean, array)     │  settings object)                    │
│ Primitive values     │ When you want object-style access    │
│                      │ (no .value)                          │
└──────────────────────┴──────────────────────────────────────┘

computed() in the Composition API

The computed() function works the same as the computed option — it calculates a value from reactive data and caches the result.

import { ref, computed } from "vue";

const firstName = ref("Jane");
const lastName  = ref("Doe");

const fullName = computed(() => {
  return firstName.value + " " + lastName.value;
});

console.log(fullName.value);  // "Jane Doe"

firstName.value = "Amy";
console.log(fullName.value);  // "Amy Doe"  (automatically updated)

Diagram: computed() Dependency and Cache

fullName depends on: firstName, lastName

firstName = "Jane", lastName = "Doe"
  → fullName.value = "Jane Doe"  (cached)

Template re-renders but firstName and lastName unchanged:
  → fullName.value = "Jane Doe"  (returns cache, no recalculation)

firstName.value = "Amy":
  → cache invalidated
  → fullName recalculates → "Amy Doe"
  → Template updates

watch() — Reacting to Changes

watch() runs a function whenever a specific reactive value changes. It is useful for side effects: saving data, making API calls, or logging.

import { ref, watch } from "vue";

const searchQuery = ref("");

watch(searchQuery, (newValue, oldValue) => {
  console.log("Search changed from:", oldValue, "to:", newValue);
  // Trigger API search here
});

watchEffect() — Automatic Dependency Tracking

watchEffect() runs immediately and re-runs automatically whenever any reactive value it reads changes — you do not specify which values to watch.

import { ref, watchEffect } from "vue";

const count = ref(0);
const multiplier = ref(2);

watchEffect(() => {
  // Runs now, and again whenever count or multiplier changes
  console.log("Result:", count.value * multiplier.value);
});

count.value = 5;       // logs: Result: 10
multiplier.value = 3;  // logs: Result: 15

Lifecycle Hooks in the Composition API

Lifecycle hooks are imported functions in the Composition API. Call them inside setup().

import { ref, onMounted, onBeforeUnmount } from "vue";

export default {
  setup() {
    const data = ref([]);
    let intervalID = null;

    onMounted(() => {
      console.log("Component is on screen");
      intervalID = setInterval(() => {
        data.value.push(Date.now());
      }, 1000);
    });

    onBeforeUnmount(() => {
      clearInterval(intervalID);
      console.log("Cleaned up before removal");
    });

    return { data };
  }
};

Lifecycle Hook Name Mapping

┌──────────────────────┬──────────────────────────────────┐
│ Options API          │ Composition API                  │
├──────────────────────┼──────────────────────────────────┤
│ created              │ (code runs directly in setup())  │
│ mounted              │ onMounted()                      │
│ beforeUpdate         │ onBeforeUpdate()                 │
│ updated              │ onUpdated()                      │
│ beforeUnmount        │ onBeforeUnmount()                │
│ unmounted            │ onUnmounted()                    │
└──────────────────────┴──────────────────────────────────┘

The script setup Shorthand

Vue 3.2 introduced <script setup> — a syntax that removes the need to write the setup() function and the return statement. Everything declared at the top level is automatically available in the template.

<template>
  <p>Count: {{ count }}</p>
  <p>Double: {{ doubled }}</p>
  <button @click="increment">Add One</button>
</template>

<script setup>
import { ref, computed } from "vue";

const count = ref(0);

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

function increment() {
  count.value++;
}
// No return statement needed
// count, doubled, increment are automatically in the template
</script>

Diagram: script setup vs Regular setup

Regular setup():
  export default {
    setup() {
      const x = ref(0);        ← declare
      return { x };            ← must return explicitly
    }
  }

<script setup>:
  const x = ref(0);            ← declare
                               ← no return needed, auto-exposed
  
  Shorter. Less boilerplate. Same behavior.
  This is the recommended modern approach.

Composables — Reusable Logic

The biggest advantage of the Composition API is composables — functions that contain reusable reactive logic. You extract feature code into a function and use it across multiple components.

Example: useCounter Composable

// 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 };
}
<!-- ComponentA.vue -->
<script setup>
import { useCounter } from "./useCounter.js";
const { count, increment, reset } = useCounter(10);
</script>

<template>
  <p>{{ count }}</p>
  <button @click="increment">+</button>
  <button @click="reset">Reset</button>
</template>

Diagram: Composable Sharing Logic

useCounter.js
  └── count (ref)
  └── increment()
  └── decrement()
  └── reset()

ComponentA.vue  →  import useCounter  →  gets its own count
ComponentB.vue  →  import useCounter  →  gets its own count
ComponentC.vue  →  import useCounter  →  gets its own count

Each component has independent reactive state.
The logic is written once, in one place.

Full Composition API Example: Search with Debounce

<template>
  <div>
    <input v-model="query" placeholder="Search products...">
    <p v-if="loading">Searching...</p>
    <ul>
      <li v-for="result in results" :key="result.id">
        {{ result.name }}
      </li>
    </ul>
  </div>
</template>

<script setup>
import { ref, watch } from "vue";

const query   = ref("");
const results = ref([]);
const loading = ref(false);

let debounceTimer = null;

watch(query, (newQuery) => {
  clearTimeout(debounceTimer);
  if (!newQuery.trim()) {
    results.value = [];
    return;
  }
  debounceTimer = setTimeout(() => {
    loading.value = true;
    // Simulate API call
    setTimeout(() => {
      results.value = [
        { id: 1, name: newQuery + " - Product A" },
        { id: 2, name: newQuery + " - Product B" }
      ];
      loading.value = false;
    }, 600);
  }, 300);
});
</script>

Diagram: Debounced Search Flow

User types "Vue":

  Type "V" → debounce starts (300ms timer)
  Type "u" → timer resets
  Type "e" → timer resets
  300ms passes with no new typing
  → API call fires
  → loading = true
  → results return after 600ms
  → loading = false
  → list shows "Vue - Product A", "Vue - Product B"

Summary

The Composition API organizes code by feature rather than by option type. The setup() function (or the cleaner <script setup> shorthand) is where all Composition API code lives. Use ref() for single reactive values, reactive() for objects, computed() for derived values, and watch() for side effects. Lifecycle hooks become imported functions like onMounted(). The greatest power of the Composition API is composables — reusable functions that encapsulate reactive logic and share it cleanly across any number of components.

Leave a Comment

Your email address will not be published. Required fields are marked *