How to migrate Options API to Composition API in Vue
Migrating from Options API to Composition API improves code organization, TypeScript support, and logic reusability across components.
As the creator of CoreUI with over 10 years of Vue.js experience since 2014, I’ve led migrations for large Vue 2 codebases to the Composition API.
The key is mapping each Options API section - data, computed, methods, watch, and lifecycle hooks - to their Composition API equivalents.
The logic becomes more collocated and easier to extract into reusable composables.
Migrate a basic component from Options to Composition API.
// ❌ Options API
export default {
data() {
return {
count: 0,
name: 'Vue'
}
},
computed: {
doubled() {
return this.count * 2
}
},
methods: {
increment() {
this.count++
}
},
mounted() {
console.log('Mounted')
}
}
// ✅ Composition API
import { ref, computed, onMounted } from 'vue'
export default {
setup() {
const count = ref(0)
const name = ref('Vue')
const doubled = computed(() => count.value * 2)
function increment() {
count.value++
}
onMounted(() => {
console.log('Mounted')
})
return { count, name, doubled, increment }
}
}
data() becomes ref() or reactive(). computed becomes computed(). methods become regular functions. Lifecycle hooks use imported functions like onMounted. Everything declared in setup must be returned to be available in the template.
Migrating Props and Emits
Handle props and emits in Composition API.
// Options API
export default {
props: {
title: { type: String, required: true },
count: { type: Number, default: 0 }
},
emits: ['update', 'close'],
methods: {
handleUpdate(value) {
this.$emit('update', value)
}
}
}
// Composition API with script setup
<script setup>
const props = defineProps({
title: { type: String, required: true },
count: { type: Number, default: 0 }
})
const emit = defineEmits(['update', 'close'])
function handleUpdate(value) {
emit('update', value)
}
</script>
defineProps replaces the props option. defineEmits replaces the emits option. With <script setup>, there’s no need to return anything - everything is automatically available in the template. This is the recommended approach for new Vue 3 components.
Migrating Watchers
Convert watch options to watch composable.
// Options API
export default {
data() {
return { query: '', results: [] }
},
watch: {
query: {
handler(newVal) {
this.fetchResults(newVal)
},
immediate: true
}
},
methods: {
async fetchResults(q) {
const res = await fetch(`/api/search?q=${q}`)
this.results = await res.json()
}
}
}
// Composition API
import { ref, watch } from 'vue'
export default {
setup() {
const query = ref('')
const results = ref([])
async function fetchResults(q) {
const res = await fetch(`/api/search?q=${q}`)
results.value = await res.json()
}
watch(query, (newVal) => {
fetchResults(newVal)
}, { immediate: true })
return { query, results }
}
}
watch() takes the reactive reference as the first argument. The handler receives the new value. Options like immediate and deep pass as a third argument. Use watchEffect for computed-style automatic dependency tracking.
Extracting into Composables
The real power: reuse logic across components.
// useSearch.js
import { ref, watch } from 'vue'
export function useSearch(endpoint) {
const query = ref('')
const results = ref([])
const loading = ref(false)
watch(query, async (newVal) => {
if (!newVal.trim()) {
results.value = []
return
}
loading.value = true
const res = await fetch(`${endpoint}?q=${newVal}`)
results.value = await res.json()
loading.value = false
}, { debounce: 300 })
return { query, results, loading }
}
// UserSearch.vue
<script setup>
import { useSearch } from './composables/useSearch'
const { query, results, loading } = useSearch('/api/users')
</script>
Extracting logic to composables is the main advantage of Composition API. The useSearch composable can be reused in any component. Data, computed values, and methods stay together by feature rather than type.
Best Practice Note
This is the same migration approach we used when updating CoreUI Vue components to Composition API. Migrate incrementally - Options API still works in Vue 3, so you don’t have to migrate everything at once. Start with new components and migrate old ones when you need to make other changes. Use <script setup> syntax for cleaner code. The migration pays dividends immediately in TypeScript projects where type inference works much better with Composition API.



