Mastering Vue 3 Composition API
A comprehensive guide to Vue 3's Composition API, covering reactivity, lifecycle hooks, and composables with practical examples.
Mastering Vue 3 Composition API
The Composition API is one of the most significant additions to Vue 3, offering a new way to organize and reuse logic in Vue components. Unlike the Options API, the Composition API allows you to group related logic together, making your code more maintainable and reusable.
Why Composition API?
Better Logic Reuse
With the Options API, sharing logic between components often required mixins or higher-order components, which could lead to naming conflicts and unclear data sources. The Composition API solves this with composables:
// useCounter.js
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 isPositive = computed(() => count.value > 0)
return {
count,
increment,
decrement,
reset,
isPositive
}
}
Better TypeScript Support
The Composition API provides superior TypeScript integration:
<script setup lang="ts">
interface User {
id: number
name: string
email: string
}
const users = ref<User[]>([])
const selectedUser = ref<User | null>(null)
// TypeScript infers types automatically
const userName = computed(() => selectedUser.value?.name ?? 'No user selected')
</script>
Core Concepts
Reactivity
Vue 3 introduces new reactivity primitives:
import { ref, reactive, computed, watch } from 'vue'
// Reactive references
const count = ref(0)
const message = ref('Hello')
// Reactive objects
const state = reactive({
user: null,
loading: false,
error: null
})
// Computed properties
const doubleCount = computed(() => count.value * 2)
// Watchers
watch(count, (newValue, oldValue) => {
console.log(`Count changed from ${oldValue} to ${newValue}`)
})
Lifecycle Hooks
Lifecycle hooks in the Composition API are imported functions:
<script setup>
import { onMounted, onUnmounted, onUpdated } from 'vue'
onMounted(() => {
console.log('Component mounted')
})
onUpdated(() => {
console.log('Component updated')
})
onUnmounted(() => {
console.log('Component unmounted')
})
</script>
Practical Examples
Form Handling
Create a reusable form composable:
// useForm.js
import { ref, reactive, computed } from 'vue'
export function useForm(initialValues = {}, validationRules = {}) {
const values = reactive({ ...initialValues })
const errors = ref({})
const touched = ref({})
const isValid = computed(() =>
Object.keys(errors.value).length === 0
)
const validate = (field) => {
if (validationRules[field]) {
const error = validationRules[field](values[field])
if (error) {
errors.value[field] = error
} else {
delete errors.value[field]
}
}
}
const handleChange = (field, value) => {
values[field] = value
touched.value[field] = true
validate(field)
}
const handleSubmit = (onSubmit) => {
// Validate all fields
Object.keys(values).forEach(validate)
if (isValid.value) {
onSubmit(values)
}
}
return {
values,
errors,
touched,
isValid,
handleChange,
handleSubmit
}
}
API Data Fetching
Create a flexible data fetching composable:
// useApi.js
import { ref, computed } from 'vue'
export function useApi(url) {
const data = ref(null)
const loading = ref(false)
const error = ref(null)
const isError = computed(() => error.value !== null)
const isEmpty = computed(() => !loading.value && !data.value)
const execute = async () => {
try {
loading.value = true
error.value = null
const response = await fetch(url)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
data.value = await response.json()
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
const reset = () => {
data.value = null
error.value = null
loading.value = false
}
return {
data,
loading,
error,
isError,
isEmpty,
execute,
reset
}
}
Migration from Options API
Here's how a typical Options API component looks when converted:
<!-- Options API -->
<script>
export default {
data() {
return {
count: 0,
message: 'Hello'
}
},
computed: {
doubleCount() {
return this.count * 2
}
},
methods: {
increment() {
this.count++
}
},
mounted() {
console.log('Component mounted')
}
}
</script>
<!-- Composition API -->
<script setup>
import { ref, computed, onMounted } from 'vue'
const count = ref(0)
const message = ref('Hello')
const doubleCount = computed(() => count.value * 2)
const increment = () => count.value++
onMounted(() => {
console.log('Component mounted')
})
</script>
Best Practices
1. Use <script setup>
The <script setup>
syntax is more concise and provides better performance:
<script setup>
// More concise than the traditional composition API syntax
const count = ref(0)
const increment = () => count.value++
</script>
2. Extract Reusable Logic
Create composables for reusable logic:
// Don't repeat yourself - create composables
const { user, login, logout } = useAuth()
const { theme, toggleTheme } = useTheme()
const { todos, addTodo, removeTodo } = useTodos()
3. Prefer ref
for Primitives
Use ref
for primitive values and reactive
for objects:
// Good
const count = ref(0)
const user = reactive({ name: 'John', age: 30 })
// Avoid
const count = reactive({ value: 0 })
const user = ref({ name: 'John', age: 30 })
Conclusion
The Composition API represents a paradigm shift in how we write Vue applications. It provides better code organization, improved reusability, and superior TypeScript support. While there's a learning curve, the benefits in terms of maintainability and developer experience make it worthwhile.
Start by converting smaller components and gradually adopt the Composition API throughout your application. Remember, you can use both APIs in the same project, making migration gradual and manageable.
Resources
- Vue 3 Composition API Guide
- Composition API RFC
- VueUse Collection - Collection of Vue Composition Utilities