Migrating Options API to Composition API Step by Step
AI generated
<v/>
{ }
Vue 3 · Composition API · Refactoring · script setup
Options API to Composition API
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.

19 min read script setup · ref · computed · composables Vue 3.4+

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.

11. FAQ: Migrating Options API to Composition API

1Do I have to migrate all components at once?
No. Both styles work in the same project in parallel. Components can be migrated whenever touched anyway.
2Which components to migrate first?
Mixin components first, then complex watch logic. Simple presentation components last or not at all.
3Why does my ref lose reactivity?
Destructuring breaks the connection to the ref. toRefs() correctly breaks apart reactive objects without losing reactivity.
4Translating watch with deep: true?
watch(source, callback, { deep: true }) corresponds to handler plus deep: true. Source must be reactive or a getter.
5What happens to created()?
No direct equivalent needed. The code just sits top-level in setup(), before any lifecycle hook.
6How do I replace mixins?
Move logic into a function with explicit return. Components destructure only what they need, no naming collisions.
7Use composables in Options API components?
Yes, via the additional setup() option. New composables can be used even in components not yet migrated.
8Why does my reactive() replacement break?
Overwriting the variable loses the proxy reference. Object.assign() or individual property assignments are the correct alternative.
9Do I strictly need script setup?
No, setup() as a function works the same. script setup is syntactic sugar, not a technical requirement.
10How long does the migration take?
No fixed deadline needed. Realistically several months alongside regular work, prioritized by mixin usage and complexity.