Vue 3 Composition API — Master Modern Component Development
Introduction: A New Way to Organize Vue Components
Vue 3 introduced the Composition API, a new way to organize and reuse component logic. Unlike the traditional Options API, the Composition API provides better organization for complex components and makes it easier to extract and reuse component logic across your application.
This comprehensive guide covers everything you need to master the Vue 3 Composition API, from basic concepts to advanced patterns.
Table of Contents
- What is the Composition API?
- Setup Function
- Reactive vs Ref
- Computed Properties
- Lifecycle Hooks
- Watchers
- Custom Hooks (Composables)
- Template Refs
- Provide and Inject
- Advanced Patterns
- Best Practices
- Conclusion
What is the Composition API?
The Composition API provides a set of functions to handle component logic, allowing you to:
- Organize code by feature rather than by type
- Reuse logic easily across components
- Better support for TypeScript
- Improved readability for complex components
Comparing APIs
// Options API
export default {
data() {
return { count: 0 };
},
methods: {
increment() { this.count++; }
},
computed: {
doubled() { return this.count * 2; }
}
}
// Composition API
import { ref, computed } from 'vue';
export default {
setup() {
const count = ref(0);
const increment = () => count.value++;
const doubled = computed(() => count.value * 2);
return { count, increment, doubled };
}
}
Setup Function
The setup() function is the entry point for the Composition API. It runs before the component is created and returns an object of properties and methods to be used in the template. This is where all your reactive state, computed properties, and methods are initialized.
import { ref, computed } from 'vue';
export default {
setup() {
// Reactive data
const count = ref(0);
const name = ref('John');
// Computed properties
const greeting = computed(() => {
return `Hello, ${name.value}!`;
});
// Methods
const increment = () => {
count.value++;
};
// Return everything that should be exposed to template
return {
count,
name,
greeting,
increment
};
}
}
Setup with Props and Context
The setup() function receives props and context objects as parameters, allowing you to access component props, emits, slots, and other context information passed from parent components.
import { computed } from 'vue';
export default {
props: ['title'],
emits: ['update'],
setup(props, { emit, slots, expose }) {
// Access props
const fullTitle = computed(() => {
return `Title: ${props.title}`;
});
// Emit events
const handleClick = () => {
emit('update', 'new value');
};
// Access slots
const hasContent = computed(() => {
return !!slots.default?.();
});
return { fullTitle, handleClick, hasContent };
}
}
Reactive vs Ref
Understanding the difference between reactive() and ref() is crucial for mastering the Composition API.
Using Ref for Primitive Values
The ref() function is ideal for wrapping primitive values (strings, numbers, booleans). It provides reactivity by accessing the underlying value through the .value property, though this is automatic in templates.
import { ref } from 'vue';
export default {
setup() {
// For primitive values
const count = ref(0);
const message = ref('Hello');
const isVisible = ref(false);
// Must use .value to access in JavaScript
const increment = () => {
count.value++;
};
// But in templates, .value is automatic
// Template: {{ count }} works directly
return { count, message, isVisible, increment };
}
}
Using Reactive for Objects
The reactive() function is better suited for complex objects with nested properties. It provides reactivity at any depth without requiring .value access in JavaScript code.
import { reactive } from 'vue';
export default {
setup() {
// For objects
const state = reactive({
count: 0,
name: 'John',
nested: {
level1: {
level2: 'deep value'
}
}
});
// No .value needed in JavaScript
const increment = () => {
state.count++;
};
// Deep properties are reactive
const updateNested = () => {
state.nested.level1.level2 = 'updated';
};
return { state, increment, updateNested };
}
}
When to Use Each
// Use ref for:
const count = ref(0); // Primitives
const items = ref([]); // Arrays
const formData = ref({}); // Objects you want to replace
// Use reactive for:
const state = reactive({ // Complex objects
user: { name: '', age: 0 },
settings: { theme: 'dark' }
});
Computed Properties
Computed properties are reactive values that automatically update when their dependencies change. They’re cached based on their dependencies, making them efficient for expensive calculations.
import { ref, computed } from 'vue';
export default {
setup() {
const firstName = ref('John');
const lastName = ref('Doe');
const age = ref(30);
// Simple computed
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`;
});
// Computed with conditional
const isAdult = computed(() => {
return age.value >= 18;
});
// Computed with getter/setter
const displayName = computed({
get() {
return `${firstName.value} ${lastName.value}`;
},
set(value) {
const [first, last] = value.split(' ');
firstName.value = first;
lastName.value = last;
}
});
return { firstName, lastName, age, fullName, isAdult, displayName };
}
}
Lifecycle Hooks
Lifecycle hooks allow you to run code at specific stages of a component’s life. These hooks execute at predictable times, from before the component mounts through to when it’s unmounted, enabling you to handle initialization, data fetching, and cleanup tasks.
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue';
export default {
setup() {
onBeforeMount(() => {
console.log('Component is about to mount');
});
onMounted(() => {
console.log('Component mounted - fetch data here');
// Fetch initial data
});
onBeforeUpdate(() => {
console.log('Component is about to update');
});
onUpdated(() => {
console.log('Component has updated');
});
onBeforeUnmount(() => {
console.log('Component is about to unmount');
});
onUnmounted(() => {
console.log('Component unmounted - cleanup here');
// Clean up subscriptions, timers, etc.
});
return {};
}
}
Watchers
Watchers enable you to run custom logic whenever a reactive value changes. They’re useful for synchronizing data, triggering API calls, or performing other side effects based on state changes.
import { ref, watch } from 'vue';
export default {
setup() {
const message = ref('');
const count = ref(0);
// Basic watcher
watch(message, (newVal, oldVal) => {
console.log(`Message changed from "${oldVal}" to "${newVal}"`);
});
// Watch multiple sources
watch([count, message], ([newCount, newMsg]) => {
console.log(`Count: ${newCount}, Message: ${newMsg}`);
});
// Watch with options
watch(message, (newVal) => {
console.log('Message:', newVal);
}, {
immediate: true, // Run immediately on creation
deep: true, // Watch nested properties
flush: 'post' // Run after component updates
});
return { message, count };
}
}
Custom Hooks (Composables)
Composables are JavaScript functions that extract and reuse component logic. They follow the naming convention use* (like useCounter, useFetch) and enable you to encapsulate and share stateful logic across multiple components.
// useCounter.js - Custom composable
import { ref, computed } from 'vue';
export function useCounter(initialValue = 0) {
const count = ref(initialValue);
const increment = () => count.value++;
const decrement = () => count.value--;
const reset = () => count.value = initialValue;
const doubled = computed(() => count.value * 2);
const isEven = computed(() => count.value % 2 === 0);
return {
count,
increment,
decrement,
reset,
doubled,
isEven
};
}
// Using the composable
import { useCounter } from './useCounter';
export default {
setup() {
const { count, increment, decrement, doubled } = useCounter(0);
return { count, increment, decrement, doubled };
}
}
Template Refs
Template refs provide a way to access DOM elements or child component instances directly. They’re useful when you need to manage focus, trigger animations, or interact with third-party libraries.
import { ref, onMounted } from 'vue';
export default {
setup() {
const inputRef = ref(null);
onMounted(() => {
// Access the DOM element
inputRef.value?.focus();
});
return { inputRef };
}
}
<template>
<input ref="inputRef" placeholder="Will auto-focus" />
</template>
Provide and Inject
Provide and Inject enable ancestor-to-descendant data sharing across the component tree without passing through intermediate components. This is useful for global configuration, themes, or shared state that multiple nested components need to access.
// Parent component
import { provide, ref } from 'vue';
export default {
setup() {
const theme = ref('dark');
const user = { id: 1, name: 'John' };
provide('theme', theme);
provide('user', user);
return {};
}
}
// Child component (any nesting level)
import { inject } from 'vue';
export default {
setup() {
const theme = inject('theme');
const user = inject('user');
return { theme, user };
}
}
Advanced Patterns
Async Setup
You can make the setup() function async to handle asynchronous operations like data fetching before the component renders. This is often used with Suspense boundaries for better loading states.
import { defineComponent } from 'vue';
export default defineComponent({
async setup() {
const data = await fetchData();
return { data };
}
})
Error Handling in Composables
Implementing error handling in composables ensures that failures (like failed API requests) are captured and exposed to components, allowing them to display error states gracefully.
import { ref } from 'vue';
export function useFetch(url) {
const data = ref(null);
const error = ref(null);
const loading = ref(true);
fetch(url)
.then(res => res.json())
.then(result => {
data.value = result;
})
.catch(err => {
error.value = err;
})
.finally(() => {
loading.value = false;
});
return { data, error, loading };
}
Best Practices
- Organize by feature: Group related logic together
- Extract composables: Reuse logic across components
- Use TypeScript: Better type safety and IDE support
- Keep setup clean: Move complex logic to separate functions
- Document composables: Clearly document expected parameters and return values
- Test composables: Unit test logic independently
Conclusion
The Composition API is a powerful tool for organizing Vue 3 components and making your code more maintainable and reusable. By mastering the concepts covered in this guide, you’ll be able to build scalable, well-organized Vue applications that are easy to maintain and extend.