The Vue 3 migration question arrives in two very different shapes. A new project asks which API to start with, and the answer is simple, the Composition API, because the ecosystem's new code, the documentation, and the component libraries have all settled there. An existing product asks something harder: a working Options API codebase with hundreds of components, and a team reading that they should now rewrite all of it. The second question is where projects are damaged, because the honest answer is that the rewrite is not required, not recommended, and in one big sprint actively harmful. This article separates the two questions, shows what the Composition API actually changes and what it leaves untouched, and builds the incremental adoption strategy that real teams use to modernize a shipping product without freezing feature work.

What the Composition API changes and what it deliberately does not

The Options API organizes a component by option type: one object for data, one for methods, one for computed properties, one for lifecycle hooks. The Composition API organizes by feature: a setup function or script setup block where the state, the computed values and the functions that belong to one concern sit together. The difference matters most as components grow, because the Options API scatters one feature across four distant blocks, while the Composition API keeps it in one place, and moves the boundaries between components toward the feature instead of the option.

<script setup>
import { ref, computed } from 'vue'

const quantity = ref(1)
const total = computed(() => quantity.value * props.price)

async function submit() {
  await api.createOrder({ sku: props.sku, quantity: quantity.value })
}
</script>

The script setup form removes the boilerplate of return statements and makes the template's bindings obvious. What does not change is equally important: the reactivity system underneath, the template syntax, the component model, the props and events contract, and the entire ecosystem of libraries. Vue 3 runs both APIs on the same engine, and the Options API is not deprecated, not slow, and not second class in any observable runtime behavior. The migration is therefore about developer ergonomics and code organization, not about correctness or performance, and that framing decides everything about its urgency.

Composables versus mixins where the real upgrade lives

The strongest argument for the Composition API in an existing product is not new syntax, it is composables, the functions that encapsulate reusable reactive logic. The Options API had mixins, and mixins were the constant source of subtle defects: two mixins could define the same data property and the collision resolved by silent overwrite, the origin of a property used in a template was invisible, and TypeScript could not follow either. The composable form carries its name explicitly, returns its own references, and composes without collisions:

// useCart.js
import { ref } from 'vue'
export function useCart() {
  const items = ref([])
  const total = computed(() =>
    items.value.reduce((sum, i) => sum + i.price, 0))
  async function add(sku) {
    items.value.push(await api.get(sku))
  }
  return { items, total, add }
}

A component calls it like any function, destructure what it needs, and the devtools show the source. The practical consequence for a migration is that composables can be adopted before components change: a team can extract a domain concern into useCart, have Options API components consume it through a thin computed or methods bridge, and only later move the components themselves. That ordering extracts the value while deferring the churn, and it is the core of the incremental strategy below.

The migration strategy that ships features the whole time

The rewrite-everything sprint fails for reasons that repeat across every team that tries it: hundreds of components change, the diff is unreviewable, the feature work freezes for a quarter, and the end state is a codebase where the new bugs live in components nobody has touched since. The strategy that works is a rule about where new code lives, plus a queue for the old. Every new component is written with script setup, no exceptions and no debate reopened per pull request, because the exception is where the codebase forks into two conventions. Every extracted or new shared behavior becomes a composable, never a mixin, and the existing mixins are ported as they are touched, which retires the legacy mechanism by attrition. Components convert opportunistically, when a feature touches one anyway, the conversion rides the same pull request as the feature change, so the reviewer sees one component changing for a reason rather than a repository-wide sweep. The largest, most feature dense components get scheduled conversions, one at a time, because they are where the Composition API pays for itself and where the Options scatter is most expensive. Trivial leaf components, the styled boxes and icon wrappers, convert last or never, because the API difference there is cosmetic and the review effort is not free.

Under those rules a product modernizes over a few quarters without a freeze, and every pull request is reviewable because it changes a component for a reason. The tracking is a label or a count in the repository, a simple grep for components still using export default with a data option, visible in the engineering dashboard, declining monotonically. The number becomes a small, motivating metric instead of a project.

One component before and after the conversion

The conversion reads better as one concrete example than as a list of differences. An Options API component with a filter and a paged list, the shape half of every real product has:

export default {
  props: ['sku'],
  data() {
    return { query: '', page: 1, items: [] }
  },
  computed: {
    filtered() {
      return this.items.filter(i => i.name.includes(this.query))
    }
  },
  methods: {
    async load() {
      this.items = await api.fetch(this.sku)
    }
  },
  watch: {
    query() { this.page = 1 }
  },
  mounted() { this.load() }
}

The same component after conversion, with the concern grouped instead of scattered:

<script setup>
import { ref, computed, watch, onMounted } from 'vue'

const props = defineProps(['sku'])
const query = ref('')
const page = ref(1)
const items = ref([])

const filtered = computed(() =>
  items.value.filter(i => i.name.includes(query.value)))

watch(query, () => { page.value = 1 })
onMounted(load)

async function load() {
  items.value = await api.fetch(props.sku)
}
</script>

The template did not change, the props contract did not change, and a component test written against the rendered behavior passes before and after without edits, which is the property that makes the opportunistic conversion safe. What changed is the organization: the query concern, the list concern and the paging concern can each be lifted into a composable by cutting along visible seams, and the moment one of them is needed in another component, the extraction is mechanical.

Reactivity in practice with refs versus the old assumptions

The Composition API surfaces Vue's reactivity directly, and that surface has a small set of rules that code review must enforce because they are where converted components go wrong. ref creates a reference, .value reads and writes it in script, and the template unwraps it automatically. reactive creates a tracked object where mutation is direct, and destructuring a reactive object loses the reactivity, which is the classic defect in converted code:

const state = reactive({ count: 0 })
const { count } = state   // count is now a plain number, will never update

The safe vocabulary is refs for primitives, reactive for grouped state or toRefs when destructuring is wanted, and computed for derived values that must not be written. Watch receives explicit dependencies instead of being implicit on every option it reads, which converts a whole class of accidental infinite loops into visible, reviewable dependency lists. None of these rules are harder than the Options API's lifecycle naming, they are just different, and a short team document with the five patterns, written once and linked from pull request templates, prevents most of the churn's defects.

TypeScript and tooling as the compounding return

The Composition API's quietest benefit is that TypeScript finally sees the code. In an Options API component, this typing is inferred at runtime through complex generic gymnastics that break at scale: props arrive as loosely typed, mixins arrive as invisible, and the compiler cannot warn about a data field renamed in one option but used in another. In script setup, types flow like ordinary JavaScript: props are declared with types, the composable's return type is checked at every call site, and a rename with a missed consumer is a red squiggle in the editor instead of a blank area at runtime.

For an existing product, the compounding is the point. Each converted component and each new composable makes the next refactor safer, because the type system carries the contract that review used to carry alone. Teams that pair the migration with TypeScript adoption convert and annotate in the same pull request, and teams that do not want both at once still get the structural benefit, since the Composition API is where the types will go when the team is ready.

The team rules and the stopping point

The adoption needs a written stopping point as much as written rules, because the goal is a codebase that works, not a codebase that is uniformly modern. The rules that hold up in review:

  1. New code is Composition API and composables, decided once, not per ticket;
  2. Old code converts when touched, and the biggest components get scheduled conversions;
  3. No mixins, new or ported, and shared reactive logic lives in composables;
  4. The reactivity rules, refs versus reactive and the destructuring trap, live in one linked document;
  5. TypeScript annotations ride along with conversion, at least for props and composable signatures;
  6. The remaining count is visible on the dashboard, and a stable small number of legacy leaf components is acceptable indefinitely.

The two APIs are not rivals, they are two generations of the same framework, and the team that treats the newer one as a tool for the code it is writing anyway gets the ergonomic and typing benefits without ever scheduling a rewrite. The product keeps shipping every sprint, and the modernization happens the only way large changes ever happen safely in software: continuously, reviewably, and in the same direction.
Modernize while you ship, and let the codebase carry both generations without shame, because the framework that runs them side by side was built exactly for that road.

The ecosystem that already made the decision

The migration is not happening in neutral territory, because the Vue ecosystem has already settled where new code lives, and the settlement makes the team's default easier to follow. Pinia, the official state store, is built as a set of composables from the ground up, a store is a defineStore function whose state, getters and actions read like the setup blocks this article has been showing, and the Options API store syntax that came before it is the legacy path. VueUse, the de facto standard library of utilities, ships one hundred plus composables for the concerns every product reinvents badly, mouse position, clipboard, intersection observation, debounce, and consuming them teaches the composable idiom faster than any style guide, because the team reads production quality examples of the pattern daily.

Nuxt, the meta framework, composes the same decision upward: its data functions, routing composables and lifecycle conventions are Composition API throughout, and a component written for the future is the component that already matches the framework's own code. The practical consequence for an Options API codebase is that the gravity is all one direction, every dependency the team adds, every example in every library's documentation, every new hire's prior experience, arrives in the newer idiom. The migration strategy from this article is really just an agreement to stop swimming against that current, one component at a time, while the ecosystem does the persuasion that no internal style guide could.