migrating step by step instead of a full rewrite
Migrating from the Options API to the Composition API does not have to mean overhauling every component at once. Vue 3 allows both styles side by side in the same project, so data, methods and computed can be translated cleanly into the Composition API component by component, while the rest of the application keeps running unchanged.
Table of Contents
- 1. Why migrate step by step instead of rewriting everything
- 2. Prioritizing migration candidates
- 3. Translating data() into ref() and reactive()
- 4. Turning methods into plain functions
- 5. computed and watch in the Composition API
- 6. Translating lifecycle hooks
- 7. Extracting recurring logic into composables
- 8. Common pitfalls during migration
- 9. Options API versus Composition API
- 10. Summary
- 11. FAQ
1. Why migrate step by step instead of rewriting everything
Migrating from the Options API to the Composition API is misunderstood by many teams as an all-or-nothing decision. The opposite is actually true: Vue 3 supports both styles in the same project, even in the same file, in parallel, which allows a step-by-step migration without losing functionality. A component still using data() and methods works right next to a sibling component that has already fully switched to script setup, without either one needing to be adjusted.
The practical benefit of this approach: a team can stretch the migration from the Options API to the Composition API over months, whenever a component is touched anyway, say for a bugfix or a new feature. This avoids the classic risk of a big refactoring sprint where hundreds of components change at once and regressions can barely be traced back to a single change. The sections below show concretely how data, methods, computed and lifecycle hooks get translated.
2. Prioritizing migration candidates
Not every component benefits equally from migrating the Options API to the Composition API. The biggest win comes from components that share a lot of logic across several places, for example via mixins, or components with a complex, hard-to-follow data() structure. Small, pure presentation components without their own logic, on the other hand, gain almost nothing from migration and can be deliberately postponed without hurting the project.
A practical prioritization criterion: migrate components with mixins first, since mixins are replaced by composables in the Composition API and gain the strongest structural improvement there. Next come components with heavy watch usage, since watch() in the Composition API is more explicit and easier to debug. Simple CRUD forms come last, since their migration is worthwhile but rarely urgent.
# Find migration candidates: components using mixins
grep -rl "mixins:" src/components/ | wc -l
# Components with heavy watch usage - good early candidates
grep -rl "watch:" src/components/ | wc -l
# Simple components without data() or methods - low priority
grep -rL "data()\|methods:" src/components/*.vue
3. Translating data() into ref() and reactive()
The entry point of migrating the Options API to the Composition API is almost always data(). Every property that data() returns becomes its own ref() call in the Composition API, or a single reactive() object for related properties. The difference is more than syntax: ref() forces access through .value in the script section, while reactive() allows direct property access but does not support replacing the whole object without losing reactivity.
The common recommendation during migration: ref() for primitive values like strings, numbers and booleans, reactive() only for objects that belong together as a whole and are rarely replaced entirely. Anyone who instead crams everything into one big reactive() object, because it resembles the old data() structure, loses some of the clarity the Composition API is supposed to bring.
// Options API: data() returning multiple properties
export default {
data() {
return {
searchTerm: '',
isLoading: false,
results: [],
filters: { category: 'all', inStock: true },
}
},
}
// Composition API: primitives as ref(), grouped object as reactive()
import { ref, reactive } from 'vue'
const searchTerm = ref('')
const isLoading = ref(false)
const results = ref([])
const filters = reactive({ category: 'all', inStock: true })
// Access in script: ref needs .value, reactive does not
searchTerm.value = 'vue migration'
filters.category = 'electronics'
4. Turning methods into plain functions
The transition from methods to plain functions is the most straightforward part of migrating the Options API to the Composition API. Every method becomes a normal JavaScript function inside setup() or script setup, without a this binding. That removes an entire class of bugs: lost this references in callbacks, arrow function traps and the need to manually bind methods with .bind(this) in event handlers all disappear.
A detail often overlooked during migration: methods that accessed data properties via this.property in the Options API access the corresponding variable directly in the Composition API, without a prefix. For ref() values, .value is still required inside the function, but not in the template, since Vue automatically unwraps refs in the template context.
// Options API: methods with this-binding
export default {
data() { return { count: 0 } },
methods: {
increment() {
this.count++
this.logChange(this.count)
},
logChange(value) {
console.log(`Count changed to ${value}`)
},
},
}
// Composition API: plain functions, no this needed
import { ref } from 'vue'
const count = ref(0)
function logChange(value) {
console.log(`Count changed to ${value}`)
}
function increment() {
count.value++
logChange(count.value) // direct call, no this.
}
5. computed and watch in the Composition API
computed properties translate almost one to one when migrating the Options API to the Composition API: a function inside the computed object becomes a computed() call returning a ref. The underlying logic stays the same, only the surrounding syntax changes. With watch, the difference is bigger: the Composition API requires an explicit source as the first argument, either a ref, a reactive property as a getter function, or an array of several sources, instead of the implicit property name as a string key in the options object.
This more explicit approach to watch() has a practical benefit during migration: it becomes immediately visible which exact source is being observed, instead of relying on a string key that can easily go stale during refactors. For deeply nested objects, { deep: true } must also be passed, which was handled through handler and deep: true in the same options object in the Options API and stays structurally very similar.
// Options API: computed and watch
export default {
data() { return { firstName: 'Anna', lastName: 'Muster', cart: { items: [] } } },
computed: {
fullName() {
return `${this.firstName} ${this.lastName}`
},
},
watch: {
firstName(newVal, oldVal) {
console.log(`Name changed from ${oldVal} to ${newVal}`)
},
cart: { handler() { this.recalculateTotal() }, deep: true },
},
}
// Composition API: computed() and watch() with explicit source
import { ref, reactive, computed, watch } from 'vue'
const firstName = ref('Anna')
const lastName = ref('Muster')
const cart = reactive({ items: [] })
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
watch(firstName, (newVal, oldVal) => {
console.log(`Name changed from ${oldVal} to ${newVal}`)
})
watch(cart, () => recalculateTotal(), { deep: true })
6. Translating lifecycle hooks
Lifecycle hooks follow a consistent naming scheme when migrating the Options API to the Composition API: mounted becomes onMounted(), updated becomes onUpdated(), beforeUnmount becomes onBeforeUnmount(). Each hook is imported as a function and called with a callback inside setup(). One important exception: created and beforeCreate have no direct equivalent, because their code simply sits directly at the top level of setup(), before any lifecycle hook is even registered.
A benefit that becomes immediately visible during migration: multiple onMounted() calls within the same component are allowed and run in sequence. That is especially useful when logic is imported from several composables, each registering its own mounted hook, without having to merge them into a single, giant mounted() handler like in the Options API.
// Options API: lifecycle hooks as object keys
export default {
created() {
console.log('created - runs before setup exists in Options API')
},
mounted() {
this.fetchData()
},
beforeUnmount() {
this.cleanupListeners()
},
}
// Composition API: imported hook functions, created logic runs top-level
import { onMounted, onBeforeUnmount } from 'vue'
// Equivalent of created() / beforeCreate(): just runs here, top-level
console.log('runs immediately when setup executes')
onMounted(() => {
fetchData()
})
onBeforeUnmount(() => {
cleanupListeners()
})
7. Extracting recurring logic into composables
The real strategic win when migrating the Options API to the Composition API rarely lies in individual components, but in the ability to extract recurring logic into composables. What was solved in the Options API as a mixin, with its known problems of unclear name origins and property collisions, becomes a function in the Composition API that returns explicitly imported, named values. A composable for pagination, form validation or API requests can be reused across any number of components without naming conflicts.
During migration it is worth translating existing mixins into composables first, before migrating the actual components. That way, components still using the Options API immediately benefit from cleaner logic too, since composables can be used in Options API components via setup(), provided setup() is defined as an additional option.
8. Common pitfalls during migration
The most common mistake when migrating the Options API to the Composition API: a ref() value gets destructured, which loses reactivity. const { count } = someObject breaks the connection to the original ref as soon as someObject is a reactive structure. The correct alternative is toRefs(), which converts a reactive object into individual refs without breaking reactivity.
A second common mistake involves reactive() objects being replaced entirely: filters = { category: 'x' } overwrites the reference and thereby loses reactivity, because Vue no longer observes the old proxy reference. The correct approach is to set individual properties or use Object.assign(filters, { category: 'x' }). Both pitfalls affect nearly every migration project and should be communicated to the team early.
9. Options API versus Composition API
The table below summarizes the key translation rules that come up in practically every component when migrating the Options API to the Composition API.
| Options API | Composition API | Key difference |
|---|---|---|
data() |
ref() / reactive() |
ref() needs .value in script |
methods |
Plain functions | No this, no bind issues |
computed |
computed() |
Nearly identical logic |
watch: { prop() {} } |
watch(source, cb) |
Explicit source instead of string key |
mixins |
Composables | Explicit return values, no collisions |
The table shows that almost every translation is syntactically manageable. The real effort when migrating the Options API to the Composition API lies not in the syntax, but in careful handling of reactivity, particularly around destructuring and object replacement.
Mironsoft
Vue 3 refactoring without big-bang rewrites
Migrating Options API to Composition API without slowing the team down?
We prioritize migration candidates, extract mixins as composables, and guide the conversion of data, methods and watch component by component.
Prioritization
Rank components by migration benefit, tackle mixins first
Composable extraction
Turn mixins and utility logic into reusable composables
Reactivity review
Check for destructuring and object replacement traps before merge
10. Summary
Migrating the Options API to the Composition API does not have to be a risky large-scale project, since Vue 3 supports both styles in parallel. The translation follows clear rules: data() becomes ref() or reactive(), methods become plain functions without this, computed stays almost identical in content, watch needs an explicit source, and lifecycle hooks follow the on-prefix scheme. Mixins are replaced by composables, solving two old problems at once: unclear name origins and property collisions.
The most reliable path is prioritizing by benefit: components with mixins and complex watch logic first, simple presentation components last or not at all. Anyone who additionally watches out for the two most common reactivity traps, destructuring refs and fully replacing reactive() objects, avoids most of the bugs that typically show up when migrating the Options API to the Composition API.
Migrating Options API to Composition API — The Essentials at a Glance
Both styles coexist
Vue 3 allows Options API and Composition API in the same project. Migrate component by component without losing functionality.
Migrate mixins first
Mixins become composables and yield the biggest structural gain. Priority over simple presentation components.
Don't break reactivity
Do not destructure refs without toRefs(). Do not replace entire reactive() objects, set properties individually instead.
Lifecycle hooks by prefix
mounted becomes onMounted(), beforeUnmount becomes onBeforeUnmount(). Multiple calls per hook are allowed.