The Vue.js 3 Composition API introduces a new way to organize component logic, making it more reusable and maintainable. Let's explore how to leverage this powerful feature effectively.
Understanding the Composition API ๐
The Composition API provides a way to write component logic using imported functions instead of declaring options objects. This approach offers better TypeScript support and more flexible code organization.
Basic Setup
<script setup>
import { ref, computed, onMounted } from 'vue'
const count = ref(0)
const doubled = computed(() => count.value * 2)
function increment() {
count.value++
}
onMounted(() => {
console.log('Component mounted')
})
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<p>Doubled: {{ doubled }}</p>
<button @click="increment">Increment</button>
</div>
</template>
Reactive State Management
Using ref and reactive
<script setup>
import { ref, reactive, toRefs } from 'vue'
const count = ref(0)
const state = reactive({
name: 'John',
age: 30,
email: 'john@example.com'
})
// Destructure reactive object while maintaining reactivity
const { name, age } = toRefs(state)
function updateProfile() {
state.age++
name.value = 'John Doe'
}
</script>
Composables (Reusable Logic) ๐
Create reusable logic with composables:
// composables/useCounter.js
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const doubled = computed(() => count.value * 2)
function increment() {
count.value++
}
function decrement() {
count.value--
}
return {
count,
doubled,
increment,
decrement
}
}
// Component usage
<script setup>
import { useCounter } from '@/composables/useCounter'
const { count, doubled, increment, decrement } = useCounter(10)
</script>
API Fetching Composable
// composables/useFetch.js
import { ref, onMounted } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(true)
async function fetchData() {
loading.value = true
try {
const response = await fetch(url)
data.value = await response.json()
} catch (e) {
error.value = e
} finally {
loading.value = false
}
}
onMounted(fetchData)
return { data, error, loading, refresh: fetchData }
}
// Usage in component
<script setup>
import { useFetch } from '@/composables/useFetch'
const { data, error, loading } = useFetch('https://api.example.com/data')
</script>
Lifecycle Hooks and Watch Effects
<script setup>
import { ref, watch, watchEffect, onMounted, onUnmounted } from 'vue'
const searchQuery = ref('')
const results = ref([])
// Watch specific reactive reference
watch(searchQuery, async (newQuery, oldQuery) => {
if (newQuery.length >= 3) {
results.value = await searchApi(newQuery)
} else {
results.value = []
}
}, { debounce: 300 })
// Automatically tracks reactive dependencies
watchEffect(() => {
console.log(`Current query: ${searchQuery.value}`)
})
// Lifecycle hooks
onMounted(() => {
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
</script>
Props and Events
<script setup>
import { computed } from 'vue'
const props = defineProps({
title: {
type: String,
required: true
},
items: {
type: Array,
default: () => []
}
})
const emit = defineEmits(['update', 'delete'])
const capitalizedTitle = computed(() => {
return props.title.charAt(0).toUpperCase() + props.title.slice(1)
})
function handleUpdate(item) {
emit('update', item)
}
</script>
Template Refs
<script setup>
import { ref, onMounted } from 'vue'
const inputRef = ref(null)
const videoRef = ref(null)
onMounted(() => {
inputRef.value.focus()
if (videoRef.value) {
videoRef.value.play()
}
})
</script>
<template>
<input ref="inputRef" type="text" />
<video ref="videoRef" src="video.mp4"></video>
</template>
Advanced Patterns
Dependency Injection
// composables/useTheme.js
import { provide, inject, ref } from 'vue'
const themeSymbol = Symbol()
export function provideTheme() {
const theme = ref('light')
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
provide(themeSymbol, {
theme,
toggleTheme
})
}
export function useTheme() {
const theme = inject(themeSymbol)
if (!theme) throw new Error('No theme provided')
return theme
}
Async Components
<script setup>
import { defineAsyncComponent } from 'vue'
const AsyncModal = defineAsyncComponent(() =>
import('./components/Modal.vue')
)
</script>
<template>
<Suspense>
<template #default>
<AsyncModal />
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
Form Handling
<script setup>
import { reactive } from 'vue'
const formState = reactive({
username: '',
email: '',
password: ''
})
const errors = reactive({})
async function handleSubmit() {
try {
await validateForm(formState)
await submitToApi(formState)
} catch (e) {
errors.value = e.errors
}
}
</script>
<template>
<form @submit.prevent="handleSubmit">
<div>
<input
v-model="formState.username"
type="text"
:class="{ error: errors.username }"
/>
<span v-if="errors.username" class="error-message">
{{ errors.username }}
</span>
</div>
<!-- Similar fields for email and password -->
</form>
</template>
Performance Optimization โก
Computed Properties vs Methods
<script setup>
import { ref, computed } from 'vue'
const items = ref([1, 2, 3, 4, 5])
// Computed property (cached)
const doubledItems = computed(() => {
return items.value.map(item => item * 2)
})
// Method (recalculated each time)
function getDoubledItems() {
return items.value.map(item => item * 2)
}
</script>
Component Optimization
<script setup>
import { shallowRef, markRaw } from 'vue'
// Use shallowRef for large objects that don't need deep reactivity
const largeData = shallowRef({ /* ... */ })
// Mark components as raw to avoid reactivity
const NonReactiveComponent = markRaw({
name: 'NonReactive',
// ...
})
</script>
Testing Composition API Components ๐งช
// counter.spec.js
import { mount } from '@vue/test-utils'
import Counter from './Counter.vue'
describe('Counter.vue', () => {
it('increments count when button is clicked', async () => {
const wrapper = mount(Counter)
expect(wrapper.text()).toContain('Count: 0')
await wrapper.find('button').trigger('click')
expect(wrapper.text()).toContain('Count: 1')
})
})
Best Practices ๐
- Organize Composables by Feature
// features/auth/useAuth.js
export function useAuth() {
// Authentication logic
}
// features/products/useProducts.js
export function useProducts() {
// Product management logic
}
- Type Safety with TypeScript
interface User {
id: number
name: string
email: string
}
const user = ref<User | null>(null)
- Error Boundaries
<script setup>
import { onErrorCaptured } from 'vue'
onErrorCaptured((err, instance, info) => {
console.error(err)
return false // Prevent error propagation
})
</script>
Conclusion
The Vue.js 3 Composition API offers a powerful way to organize component logic. Remember to:
- Use composables for reusable logic
- Leverage TypeScript for better type safety
- Implement proper error handling
- Optimize performance where needed
- Write comprehensive tests
- Follow Vue.js best practices
These practices will help you build maintainable and scalable Vue.js applications using the Composition API.