Last updated: April 13, 2025
Table of Contents
1. Introduction: The Power of Composition API
Vue 3 introduced the Composition API as a flexible alternative to the traditional Options API for organizing component logic. It addresses limitations of the Options API in large components and enhances logic reuse and TypeScript integration. While the Options API remains valid, the Composition API, especially when used with the <script setup>
syntax, enables powerful patterns for building scalable and maintainable Vue applications. This article explores key patterns you can leverage with the Composition API.
2. Core Reactivity Patterns (<script setup>)
The foundation of the Composition API is its reactivity system. <script setup>
provides a concise way to declare reactive state directly within the script block.
2.1 ref
: Reactivity for Any Value
ref
is used to create a reactive variable for any value type, including primitives (strings, numbers, booleans) and objects. To access or modify the value of a ref, you must use its .value
property within the script block. In the template, Vue automatically unwraps the ref, so .value
is not needed.
<script setup>
import { ref } from 'vue';
const count = ref(0); // Reactive reference for a number
const message = ref('Hello Vue!'); // Reactive reference for a string
function increment() {
count.value++; // Access and modify using .value
}
</script>
<template>
<div>
<p>{{ message }}</p>
<p>Count: {{ count }}</p> <!-- No .value needed in template -->
<button @click="increment">Increment</button>
</div>
</template>
2.2 reactive
: Reactivity for Objects
reactive
is specifically designed to make JavaScript objects deeply reactive. Unlike ref
, you don't need .value
to access or modify properties of a reactive object. However, reactive
has limitations: it only works for object types (Objects, Arrays, Map, Set) and loses reactivity if the entire object is replaced.
<script setup>
import { reactive } from 'vue';
const user = reactive({
name: 'Alice',
age: 30,
address: {
city: 'Wonderland'
}
});
function celebrateBirthday() {
user.age++; // Direct mutation works
}
function changeCity(newCity) {
user.address.city = newCity; // Deep reactivity works
}
// Caution: Replacing the entire object breaks reactivity
// function replaceUser() {
// user = reactive({ name: 'Bob', age: 40 }); // This does NOT work as expected
// }
</script>
<template>
<div>
<p>Name: {{ user.name }}</p>
<p>Age: {{ user.age }}</p>
<p>City: {{ user.address.city }}</p>
<button @click="celebrateBirthday">Birthday!</button>
<button @click="changeCity('Metropolis')">Move City</button>
</div>
</template>
Generally, ref
is recommended as the primary choice for declaring reactive state due to its flexibility, even for objects.
3. Computed Properties (computed
)
Computed properties allow you to declaratively create derived values based on other reactive state. They are cached based on their reactive dependencies and only re-evaluate when needed.
3.1 Deriving State Efficiently
Use computed
to create a reactive reference whose .value
is determined by a getter function.
<script setup>
import { ref, computed } from 'vue';
const firstName = ref('John');
const lastName = ref('Doe');
// Computed property combines firstName and lastName
const fullName = computed(() => {
console.log('Calculating fullName...'); // Will only log when firstName or lastName changes
return `${firstName.value} ${lastName.value}`;
});
function changeName() {
firstName.value = 'Jane';
}
</script>
<template>
<div>
<p>First Name: <input v-model="firstName"></p>
<p>Last Name: <input v-model="lastName"></p>
<p>Full Name: {{ fullName }}</p> <!-- Accessed like a ref -->
<button @click="changeName">Change First Name</button>
</div>
</template>
3.2 Writable Computed Properties
While less common, you can create a computed property that can also be set by providing both a getter and a setter.
<script setup>
import { ref, computed } from 'vue';
const firstName = ref('John');
const lastName = ref('Doe');
const writableFullName = computed({
// Getter
get() {
return `${firstName.value} ${lastName.value}`;
},
// Setter
set(newValue) {
// Assumes format "FirstName LastName"
const names = newValue.split(' ');
firstName.value = names[0] || '';
lastName.value = names[1] || '';
}
});
</script>
<template>
<div>
<p>Set Full Name: <input v-model="writableFullName"></p>
<p>First Name: {{ firstName }}</p>
<p>Last Name: {{ lastName }}</p>
</div>
</template>
4. Watching State (watch
& watchEffect
)
Watchers allow you to perform side effects in response to changes in reactive state. There are two main functions: watch
and watchEffect
.
4.1 watch
: Fine-Grained Control
watch
requires explicitly specifying the reactive source(s) to watch. It provides the previous and current values and offers options like deep
(for objects/arrays) and immediate
(to run the callback immediately on creation).
<script setup>
import { ref, watch } from 'vue';
const question = ref('');
const answer = ref('Questions usually contain a question mark. ;-)');
// Watch the 'question' ref
watch(question, async (newQuestion, oldQuestion) => {
console.log(`Old: ${oldQuestion}, New: ${newQuestion}`);
if (newQuestion.includes('?')) {
answer.value = 'Thinking...';
try {
// Example API call (replace with actual fetch if needed)
// const res = await fetch('https://yesno.wtf/api');
// answer.value = (await res.json()).answer;
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate API delay
answer.value = Math.random() > 0.5 ? 'Yes!' : 'No!';
} catch (error) {
answer.value = 'Error! Could not reach the API.';
}
} else {
answer.value = 'Questions usually contain a question mark. ;-)';
}
});
// Watching reactive object properties requires a getter function
const user = reactive({ name: 'Alice', age: 30 });
watch(
() => user.age, // Watch specific property
(newAge, oldAge) => {
console.log(`Age changed from ${oldAge} to ${newAge}`);
}
);
// Use { deep: true } to watch all nested properties of an object/array
// watch(user, (newUser, oldUser) => { ... }, { deep: true });
</script>
<template>
<p>
Ask a yes/no question:
<input v-model="question" />
</p>
<p>{{ answer }}</p>
</template>
4.2 watchEffect
: Automatic Dependency Tracking
watchEffect
runs a function immediately and automatically re-runs it whenever any of its reactive dependencies change. It's simpler for cases where you just need to react to *any* relevant change, without needing the old/new values or fine-grained control.
<script setup>
import { ref, watchEffect } from 'vue';
const userId = ref(1);
const userData = ref(null);
// Runs immediately, and re-runs whenever userId changes
watchEffect(async () => {
console.log(`Workspaceing data for user ${userId.value}`);
try {
// Example: Simulate fetch based on userId
await new Promise(resolve => setTimeout(resolve, 500));
userData.value = { id: userId.value, name: `User ${userId.value}` };
console.log('User data fetched:', userData.value);
} catch(e) {
console.error("Failed to fetch user data");
userData.value = null;
}
});
function loadNextUser() {
userId.value++;
}
</script>
<template>
<div>
<p v-if="userData">Current User: {{ userData.name }} (ID: {{ userData.id }})</p>
<p v-else>Loading user data...</p>
<button @click="loadNextUser">Load Next User</button>
</div>
</template>
5. Lifecycle Hooks within setup
Composition API provides functions for registering lifecycle hooks directly within the setup
context (or <script setup>
).
5.1 Mapping Options API Hooks
Most Options API lifecycle hooks have corresponding Composition API functions prefixed with on
:
beforeCreate
-> Not needed (usesetup
itself)created
-> Not needed (usesetup
itself)beforeMount
->onBeforeMount
mounted
->onMounted
beforeUpdate
->onBeforeUpdate
updated
->onUpdated
beforeUnmount
->onBeforeUnmount
unmounted
->onUnmounted
errorCaptured
->onErrorCaptured
renderTracked
->onRenderTracked
(Debug only)renderTriggered
->onRenderTriggered
(Debug only)
5.2 Usage Example
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const timer = ref(0);
let intervalId = null;
console.log('Component setup initiated (like created/beforeCreate)');
onMounted(() => {
console.log('Component has been mounted!');
intervalId = setInterval(() => {
timer.value++;
}, 1000);
});
onUnmounted(() => {
console.log('Component is about to be unmounted!');
clearInterval(intervalId);
});
</script>
<template>
<div>
<p>Timer running since mount: {{ timer }} seconds</p>
</div>
</template>
6. Reusable Logic with Composables
Composables are the primary pattern for extracting and reusing stateful logic in Vue 3's Composition API.
6.1 What are Composables?
A composable is essentially a function that leverages Composition API functions (ref
, computed
, watch
, lifecycle hooks, etc.) to encapsulate state and logic. By convention, their names start with use
(e.g., useMousePosition
, useFetch
). They can accept arguments and return reactive state, computed values, or methods.
6.2 Example: useMousePosition
Composable
Let's create a composable function that tracks the mouse position.
Create a file (e.g., src/composables/useMousePosition.js
):
// src/composables/useMousePosition.js
import { ref, onMounted, onUnmounted } from 'vue';
// Composable function
export function useMousePosition() {
// State encapsulated and managed by the composable
const x = ref(0);
const y = ref(0);
// Composable can encapsulate its own lifecycle logic
function update(event) {
x.value = event.pageX;
y.value = event.pageY;
}
onMounted(() => {
window.addEventListener('mousemove', update);
});
onUnmounted(() => {
window.removeEventListener('mousemove', update);
});
// Expose managed state as return value
return { x, y };
}
Now, use this composable in any component:
<!-- MyComponent.vue -->
<script setup>
import { useMousePosition } from '@/composables/useMousePosition'; // Adjust path as needed
// Use the composable to get reactive state
const { x, y } = useMousePosition();
</script>
<template>
<div>
Mouse position is at: {{ x }}, {{ y }}
</div>
</template>
This pattern makes it incredibly easy to share complex, stateful logic across multiple components without mixins or complex inheritance structures.
7. Conclusion
Vue 3's Composition API, particularly with <script setup>
, offers a powerful and elegant way to structure component logic. By mastering patterns around reactivity (ref
, reactive
), derived state (computed
), side effects (watch
, watchEffect
), lifecycle management (onMounted
, etc.), and logic reuse (composables), you can build more organized, maintainable, and scalable Vue applications. It's particularly beneficial for larger projects, teams, and those leveraging TypeScript.
8. Additional Resources
Related Articles
- Frontend State Management Approaches
- JavaScript Framework Comparison
- Introduction to TypeScript
- JavaScript: var vs let vs const Explained