Vue.js Composition API
The Composition API is a powerful feature in Vue.js 3 that provides a flexible way to organize and reuse logic in your components. Unlike the Options API, which groups options by type (data, methods, computed, etc.), the Composition API lets you organize code by logical concerns.
What is the Composition API?
The Composition API is an alternative way to write Vue components that uses functions instead of object properties. It was introduced in Vue.js 3 to address scalability issues with larger components and make logic reuse more intuitive.
Basic Setup with <script setup>
The <script setup> syntax is the recommended way to use the Composition API. It provides a more concise and ergonomic syntax.
<template>
<div>
<h1>{{ message }}</h1>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
<button @click="decrement">Decrement</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
// Reactive state
const message = ref('Hello, Vue 3!')
const count = ref(0)
// Methods
function increment() {
count.value++
}
function decrement() {
count.value--
}
</script>Reactive Data with ref() and reactive()
Using ref()
ref() creates a reactive reference to a value. It’s perfect for primitive values like strings, numbers, and booleans.
<script setup>
import { ref } from 'vue'
const name = ref('John Doe')
const age = ref(25)
const isVisible = ref(true)
// Access ref values with .value
function updateName() {
name.value = 'Jane Smith'
}
</script>Using reactive()
reactive() creates a reactive object. It’s best used for objects and arrays.
<script setup>
import { reactive } from 'vue'
const user = reactive({
name: 'John Doe',
email: '[email protected]',
address: {
city: 'New York',
country: 'USA'
}
})
function updateUser() {
user.name = 'Jane Smith'
user.email = '[email protected]'
}
</script>Computed Properties
Computed properties automatically update when their dependencies change.
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`
})
// Writable computed property
const fullNameWritable = computed({
get() {
return `${firstName.value} ${lastName.value}`
},
set(newValue) {
const names = newValue.split(' ')
firstName.value = names[0]
lastName.value = names[names.length - 1]
}
})
</script>Watchers
Watchers let you perform side effects when reactive data changes.
<script setup>
import { ref, watch, watchEffect } from 'vue'
const searchQuery = ref('')
const results = ref([])
// Watch a specific ref
watch(searchQuery, (newValue, oldValue) => {
console.log(`Search changed from "${oldValue}" to "${newValue}"`)
// Perform API call or other side effect
})
// Watch multiple sources
watch([searchQuery, results], ([newQuery, newResults], [oldQuery, oldResults]) => {
console.log('Either query or results changed')
})
// WatchEffect automatically tracks dependencies
watchEffect(() => {
console.log(`Current search: ${searchQuery.value}`)
// This will re-run whenever searchQuery changes
})
</script>Lifecycle Hooks
Lifecycle hooks in the Composition API are imported from vue and called within <script setup>.
<script setup>
import { onMounted, onUnmounted, onUpdated } from 'vue'
onMounted(() => {
console.log('Component is mounted')
// Perfect for API calls, setting up timers, etc.
})
onUnmounted(() => {
console.log('Component is unmounted')
// Clean up timers, event listeners, etc.
})
onUpdated(() => {
console.log('Component is updated')
})
</script>Props and Emits
Define props and emits using defineProps() and defineEmits().
<template>
<div>
<h2>{{ title }}</h2>
<p>Received message: {{ message }}</p>
<button @click="notifyParent">Notify Parent</button>
</div>
</template>
<script setup>
// Define props
const props = defineProps({
title: {
type: String,
required: true
},
message: {
type: String,
default: 'No message'
}
})
// Define emits
const emit = defineEmits(['update', 'notify'])
// Use props in template or script
console.log(props.title)
// Emit events to parent
function notifyParent() {
emit('notify', 'Child component message')
emit('update', { timestamp: Date.now() })
}
</script>Composables (Reusable Logic)
Composables are functions that encapsulate and reuse logic across components.
// 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--
}
function reset() {
count.value = initialValue
}
return {
count,
doubled,
increment,
decrement,
reset
}
}Using the composable in a component:
<script setup>
import { useCounter } from '@/composables/useCounter'
const { count, doubled, increment, decrement, reset } = useCounter(10)
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<p>Doubled: {{ doubled }}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
<button @click="reset">Reset</button>
</div>
</template>Provide and Inject
Use provide() and inject() for dependency injection across component hierarchies.
<!-- ParentComponent.vue -->
<script setup>
import { provide, ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
const theme = ref('dark')
const user = ref({ name: 'John Doe' })
provide('theme', theme)
provide('user', user)
</script>
<template>
<div>
<ChildComponent />
</div>
</template><!-- ChildComponent.vue -->
<script setup>
import { inject } from 'vue'
const theme = inject('theme')
const user = inject('user')
// Provide default values
const config = inject('config', { apiKey: 'default' })
</script>
<template>
<div>
<p>Current theme: {{ theme }}</p>
<p>User: {{ user.name }}</p>
</div>
</template>Template Refs
Access DOM elements and component instances using template refs.
<template>
<div>
<input ref="inputRef" type="text" />
<button @click="focusInput">Focus Input</button>
<ChildComponent ref="childRef" />
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import ChildComponent from './ChildComponent.vue'
const inputRef = ref(null)
const childRef = ref(null)
onMounted(() => {
inputRef.value.focus()
childRef.value.someMethod()
})
function focusInput() {
inputRef.value.focus()
}
</script>When to Use Composition API
The Composition API is particularly useful for:
- Large components - Better organization of complex logic
- Code reuse - Composables make sharing logic much easier
- TypeScript support - Better type inference and autocompletion
- Complex state management - Easier to manage related reactive data
Comparison with Options API
| Feature | Composition API | Options API |
|---|---|---|
| Organization | By logical concern | By option type |
| Code reuse | Composables | Mixins |
| TypeScript | Excellent support | Limited support |
| Learning curve | Steeper initially | More familiar |
| Component size | Better for large components | Good for simple components |
Best Practices
- Use
<script setup>for cleaner code - Create composables for reusable logic
- Group related code together
- Use
ref()for primitives andreactive()for objects - Extract complex logic into composables
- TypeScript users should definitely use Composition API
Common Patterns
API Data Fetching
// 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() {
try {
loading.value = true
const response = await fetch(url)
data.value = await response.json()
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
onMounted(fetchData)
return {
data,
error,
loading,
refetch: fetchData
}
}Local Storage Sync
// composables/useLocalStorage.js
import { ref, watch } from 'vue'
export function useLocalStorage(key, defaultValue) {
const storedValue = localStorage.getItem(key)
const value = ref(storedValue ? JSON.parse(storedValue) : defaultValue)
watch(value, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue))
}, { deep: true })
return value
}External Resources:
Related Tutorials: