If you have been building web applications with Vue.js for a while, you are likely familiar with the Options API. It is intuitive, organized by data, methods, and computed properties, and has helped millions of developers get started. However, as applications grow, the Options API can lead to “giant components” where logic for a single feature is scattered across different parts of the file. This makes maintenance a nightmare and code reuse difficult.
The Vue.js Composition API, introduced in Vue 3, was designed to solve these exact problems. It provides a more flexible way to organize your code, allowing you to group related logic together. Whether you are a beginner looking to understand the core concepts or an intermediate developer aiming to master composables, this guide will walk you through everything you need to know about the Composition API.
The Problem: The “Scattered Logic” of Options API
In the traditional Options API, logic is organized by component options. Imagine a component that handles two features: User Profile Management and Search Functionality. In an Options API component, the variables for searching would be in data, the search logic in methods, and the search result filtering in computed. Meanwhile, the profile data and methods would be mixed in those same sections.
As the component grows to 500+ lines of code, you find yourself jumping up and down the file to understand how a single feature works. This fragmentation is what the Composition API aims to fix by allowing us to group code by logical concern rather than option type.
Core Concepts of the Composition API
To master the Composition API, we must first understand its building blocks. Unlike the Options API, where Vue handles the reactivity “behind the scenes” based on where you put your variables, the Composition API requires us to be more explicit using functions like ref and reactive.
1. The Setup Function
The setup() function is the entry point for the Composition API in a component. It is executed before the component is created, once the props are resolved. In modern Vue development, we use the <script setup> syntax, which is a compile-time syntactic sugar that makes the code significantly more concise.
// Traditional setup() function
export default {
setup() {
const message = 'Hello Vue 3!';
// We must return what we want to expose to the template
return {
message
};
}
}
2. Reactivity with ref()
In Vue 3, a primitive value (like a string, number, or boolean) is not reactive by default. To make it reactive, we use the ref() function. This creates a “reference” to the value. To access or change the value inside the script, you must use .value.
import { ref } from 'vue';
// Define a reactive variable
const counter = ref(0);
// Update the value
const increment = () => {
counter.value++; // Inside script, we use .value
};
// In the template, Vue automatically unwraps the ref, so counter is used directly
3. Reactivity with reactive()
The reactive() function is used for complex types like objects and arrays. Unlike ref, you don’t need to use .value to access properties. However, it has a major limitation: it only works for objects, and you cannot easily destructure them without losing reactivity.
import { reactive } from 'vue';
const state = reactive({
user: 'John Doe',
points: 100
});
// Update state
state.points += 10;
Choosing Between Ref and Reactive
A common question for beginners is: When should I use ref vs reactive?
- Use ref() for primitives (string, number, boolean) and whenever you might need to reassign an entire object or array. Many developers prefer using
reffor everything to stay consistent. - Use reactive() for deeply nested objects where you want to avoid
.valueand the object structure remains stable.
Pro Tip: Most Vue experts recommend sticking with ref for almost everything. It makes it clearer which variables are reactive (because of the .value syntax) and prevents issues when destructuring.
Computed Properties and Watchers
The Composition API provides standalone functions for computed properties and watchers, which behave similarly to their Options API counterparts but can be used anywhere.
Computed Properties
A computed() function takes a getter function and returns a read-only reactive reference to the result.
import { ref, computed } from 'vue';
const firstName = ref('Jane');
const lastName = ref('Doe');
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`;
});
Watchers
Watchers allow us to perform side effects when data changes (e.g., API calls, logging). The watch() function takes a source and a callback.
import { ref, watch } from 'vue';
const searchInput = ref('');
watch(searchInput, (newValue, oldValue) => {
console.log(`Search changed from ${oldValue} to ${newValue}`);
// Perform API call here
});
Lifecycle Hooks in Composition API
Lifecycle hooks are renamed slightly and must be imported. For example, mounted() becomes onMounted(), and updated() becomes onUpdated(). These hooks are called inside the setup() function.
| Options API | Composition API |
|---|---|
| beforeCreate / created | Not needed (use setup/script setup) |
| mounted | onMounted |
| unmounted | onUnmounted |
| errorCaptured | onErrorCaptured |
The Power of Composables (Logic Reuse)
This is where the Composition API truly shines. A Composable is a function that leverages the Composition API to encapsulate and reuse stateful logic. In the Options API, we used Mixins, which were often confusing due to property name collisions and “invisible” data sources. Composables solve this by using standard JavaScript imports/exports.
Creating Your First Composable: useFetch
Let’s build a reusable logic for fetching data from an API. We’ll name it useFetch.js. By convention, composable names start with “use”.
// useFetch.js
import { ref, watchEffect } from 'vue';
export function useFetch(url) {
const data = ref(null);
const error = ref(null);
const loading = ref(true);
const fetchData = async () => {
loading.value = true;
try {
const res = await fetch(url);
data.value = await res.json();
} catch (err) {
error.value = err;
} finally {
loading.value = false;
}
};
fetchData();
return { data, error, loading };
}
Now, we can use this logic in any component effortlessly:
<script setup>
import { useFetch } from './composables/useFetch';
const { data, error, loading } = useFetch('https://api.example.com/users');
</script>
<template>
<div v-if="loading">Loading...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<ul v-else>
<li v-for="user in data" :key="user.id">{{ user.name }}</li>
</ul>
</template>
Step-by-Step Instruction: Building a Reactive Search System
Let’s combine everything we’ve learned into a real-world scenario: A component that filters a list of products based on a search term and a category, while keeping the logic clean.
Step 1: Set up the Reactive State
First, we define our raw data and the search criteria using ref.
import { ref, computed } from 'vue';
const products = ref([
{ id: 1, name: 'Laptop', category: 'Electronics' },
{ id: 2, name: 'Coffee Mug', category: 'Home' },
{ id: 3, name: 'Keyboard', category: 'Electronics' },
]);
const searchQuery = ref('');
const selectedCategory = ref('All');
Step 2: Create the Filtering Logic
We use a computed property so that the list updates automatically whenever searchQuery or selectedCategory changes.
const filteredProducts = computed(() => {
return products.value.filter(product => {
const matchesSearch = product.name.toLowerCase().includes(searchQuery.value.toLowerCase());
const matchesCategory = selectedCategory.value === 'All' || product.category === selectedCategory.value;
return matchesSearch && matchesCategory;
});
});
Step 3: Implement Lifecycle Logging
Let’s log a message when the user starts searching to demonstrate hooks.
import { onMounted } from 'vue';
onMounted(() => {
console.log('Search Component is ready!');
});
Step 4: Putting it in the Template
Notice how clean the template remains because all the complex logic is handled in the <script setup> block.
<template>
<input v-model="searchQuery" placeholder="Search products..." />
<select v-model="selectedCategory">
<option>All</option>
<option>Electronics</option>
<option>Home</option>
</select>
<ul>
<li v-for="p in filteredProducts" :key="p.id">{{ p.name }}</li>
</ul>
</template>
Common Mistakes and How to Avoid Them
1. Forgetting `.value` in Script
This is the #1 mistake for developers moving to Vue 3. Because a ref is an object wrapper, you cannot use it directly in JavaScript logic without accessing its value.
Wrong: if (myRef === true)
Right: if (myRef.value === true)
Note: You do NOT need .value in the template section.
2. Destructuring Reactive Objects
If you destructure a reactive object, the resulting variables lose their connection to the original object and are no longer reactive.
const state = reactive({ count: 0 });
let { count } = state; // This is now just a plain number
count++; // state.count will NOT change!
Fix: Use toRefs() to safely destructure while maintaining reactivity.
import { toRefs } from 'vue';
const { count } = toRefs(state); // Now 'count' is a ref
3. Using reactive() for Primitives
Passing a string or number to reactive() will not work as expected and will trigger a warning in the console. Always use ref() for single values.
Summary / Key Takeaways
- Organization: The Composition API allows you to group code by feature, making components more readable and maintainable.
- Reactivity: Use
ref()for simple values andreactive()for objects. Remember the.valueproperty in your scripts. - Reusability: “Composables” are the modern replacement for Mixins. They allow you to share stateful logic between components easily.
- Cleanliness: Using
<script setup>reduces boilerplate and makes your Vue files much cleaner. - Future-Proofing: The Composition API is the primary focus of the Vue team and offers superior TypeScript support compared to the Options API.
Frequently Asked Questions (FAQ)
Q1: Is the Options API being deprecated?
No. The Options API is still fully supported and is not going anywhere. However, for large-scale enterprise applications or projects where logic reuse is a priority, the Composition API is highly recommended.
Q2: Can I use both APIs in the same component?
Yes, technically you can use the setup() function alongside the Options API. However, it is generally considered a bad practice as it creates confusion and makes the code harder to follow. It is better to pick one style for a single component.
Q3: Does Composition API make the bundle size larger?
Actually, it’s the opposite! Code written with the Composition API is more “tree-shakeable.” This means modern build tools can better identify and remove unused code, potentially leading to smaller production bundles compared to the Options API.
Q4: Why does Vue use .value instead of just making variables reactive?
In JavaScript, primitive values (like strings) are passed by value, not by reference. To make them reactive, Vue must wrap them in an object so it can track changes. The .value is the key that holds the actual data.
Q5: Is Composition API harder to learn?
It has a slightly steeper learning curve because it requires a better understanding of how JavaScript references work. However, once you understand the concept of ref and composables, most developers find it more powerful and logical than the Options API.
Final Thoughts
Mastering the Composition API is a significant milestone for any Vue.js developer. It shifts your mindset from “where does this variable go?” to “what does this feature do?”. By embracing composables and explicit reactivity, you will build applications that are easier to test, easier to scale, and more enjoyable to write.
Start small by refactoring a single complex component, and soon you’ll find that the Composition API becomes your preferred way to build for the modern web.
