Vue 2 reached end of life on December 31, 2023. That deadline turned a lot of "we'll get to it" Vue 3 migrations into "we need this in the next sprint" Vue 3 migrations, and the projects that started early had a much better time than the projects that didn't.
The thing nobody tells you about a Vue 3 migration is that the framework itself is rarely the hard part. The Composition API is opt-in. The template syntax is mostly the same. The real cost lives in everything around Vue: the plugins that haven't been updated, the build tooling that was pinned in 2019, the test setup that quietly relies on a Vue 2 internal, and the tribal knowledge that lives in your team's reflexes.
This piece is the post-mortem version. The mistakes I've watched teams repeat, the tools that actually help, and the habits that make the upgrade survivable.
The Migration Build Is A Bridge, Not A Destination
The official @vue/compat migration build runs Vue 2 code on Vue 3 and warns about every breakage along the way. It is, genuinely, one of the most considerate framework migrations I've used. You install it as a Vue 3 alias, set per-component or global compat flags, and watch your console fill with deprecation messages.
// vite.config.js
export default {
resolve: {
alias: { vue: '@vue/compat' },
},
optimizeDeps: { exclude: ['vue'] },
}
The trap is treating compat mode as a finishing line. It isn't. It's an airlock. The longer the codebase sits in compat mode, the more new code gets written that quietly relies on Vue 2 semantics, and the bigger the eventual full upgrade becomes. Set a date. Track unresolved warnings. Remove the alias.
If you can't make the jump in one go, Vue 2.7 is a useful stepping stone. It backported the Composition API, <script setup>, and most of the Vue 3 type system to Vue 2. Refactor toward Composition API on Vue 2.7, then swap the engine. Vue 2.7 itself reached EOL at the end of 2023, so this is no longer a place to live — only a place to pass through.
Removed APIs You Will Hit
The breakages that show up in almost every legacy codebase, in roughly the order they bite:
- Filters are gone.
{{ price | currency }}no longer compiles. Replace with computed properties or method calls. The codemod is mechanical but tedious; do it in a single PR per module. $listenersmerged into$attrs. Components that didv-on="$listeners"needv-bind="$attrs"(which now includes listeners asonClick,onSubmit, etc.). Combined with the newinheritAttrs: falsesemantics, root-element forwarding is the main thing to test.- Slot syntax. The old
slotandslot-scopeattributes are removed in favour ofv-slot:name="props"(and the#name="props"shorthand). The behaviour is the same; the syntax isn't. - Functional components.
functional: truesingle-file components no longer exist. Plain function components work, but most teams just convert them back to regular SFCs. Vue.set/Vue.delete. Vue 3 reactivity is proxy-based, so the compatibility helpers aren't needed. Replace with normal assignment.- Event bus.
new Vue()as an event bus doesn't work becauseVueis no longer a constructor in the same way. Replace withmitt, a Pinia store, or — usually better — props and emits.
The migration build flags every one of these with a console warning. Treat the warning list like a backlog: triage, group, ship.
Vuex To Pinia Is A Refactor, Not A Rename
Vuex 4 supports Vue 3, so you don't have to migrate to Pinia to ship the upgrade. You probably should anyway. Pinia is the officially recommended store now, the type inference is dramatically better, and the setup-style stores look like normal composables.
// stores/auth.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useAuthStore = defineStore('auth', () => {
const user = ref<User | null>(null)
const isAuthenticated = computed(() => user.value !== null)
async function login(email: string, password: string) {
user.value = await api.login(email, password)
}
function logout() {
user.value = null
}
return { user, isAuthenticated, login, logout }
})
The migration pattern that works: keep Vuex installed, introduce Pinia alongside it, move modules one at a time starting with the leaves of the dependency graph. Don't rewrite a global cart store first; rewrite the settings module nobody touches. Get the team comfortable with the shape, then move the busy parts.
Tooling Is Half The Migration
The thing that surprises every team I've worked with: the Vue version isn't what slows the upgrade down. The plugin ecosystem is.
Vetur, the original VS Code extension, was replaced by Volar (now called Vue Language Tools). Uninstall Vetur. The two extensions disagree about template parsing in ways that produce hours of wasted debugging.
ESLint plugins need updating to versions that understand Composition API and <script setup>. The eslint-plugin-vue rules for Vue 3 are a different rule set; do not assume your .eslintrc carries over.
Test setup is where teams lose the most time. @vue/test-utils v2 is the Vue 3 version; v1 is Vue 2. The mounting API is similar but not identical, especially around slots, propsData (renamed props), and global plugins. Run the suite early and budget for the rewrites — usually small, sometimes structural.
The Composition API Refactor Is Optional
Vue 3 does not require the Composition API. Options API still works, still has full type support, and still ships in the Vue 3 documentation as a first-class style. You can run a Vue 3 codebase that looks identical to a Vue 2 codebase, modulo the breaking changes above.
That changes the migration calculus. The minimum migration is "Vue 2 syntax, Vue 3 engine." That's a real, shippable target. The Composition API rewrite is a separate project, done component by component as you touch them. Bundling the two is the most common reason migrations stall: the team tries to redesign every component while also fighting build errors, and ships nothing for a quarter.
I have a separate piece on refactoring Options API to Composition API safely — the short version is "do it incrementally, with tests, and stop at the boundary of the next feature."
The Migration Order That Works
The sequence I've watched succeed, three times in a row:
- Update tooling first. Volar, ESLint,
@vue/test-utils, Vite (or Vue CLI on the latest Vue-3-compatible release). Get the new build green on the existing Vue 2 code. - Audit dependencies. Every Vue plugin in
package.jsonneeds a Vue 3 compatible version. List the ones that don't have one. Decide: replace, fork, or remove. This usually surfaces 2-3 plugins that block the upgrade and need real work. - Switch to the migration build. Watch the warnings. Fix the categorical ones (filters, slots, listeners) in batches.
- Run tests. Fix the test rewrites. Don't ship until the suite is green.
- Migrate the router. Vue Router 4 is the Vue 3 version. The API surface is similar; the install and the typing aren't.
- Migrate the store. Vuex 4 first if you must, Pinia if you can.
- Drop the migration build. Switch the alias from
@vue/compattovue. Fix whatever was relying on the compat layer. - Then, and only then, start the Composition API refactor. Per component, per feature, on the cadence of normal work.
Big-bang rewrites feel decisive in a planning meeting and fall apart in week three. The incremental sequence ships small, observable changes that the team can review and roll back.
What You Will Learn About Your Codebase
Every migration I've worked on has surfaced the same thing: the code that was hardest to migrate was the code that was already hard to maintain. Tightly coupled mixins, components that read directly from the Vuex internal state, helpers that mutated reactive objects in ways the original author barely remembered.
Vue 3 is stricter about a few things — proxy-based reactivity catches some patterns that Vue 2's Object.defineProperty quietly tolerated. That strictness is a feature. The components that broke during my last migration were the components that probably should have broken three years ago.
If the migration is exposing structural problems, those problems were there before the migration. Vue 3 just turned them into compiler errors.
The Honest Summary
A Vue 3 migration is mostly a tooling and dependency project, with a small amount of framework code on top. The teams that finish it on time treat it that way. They lock the scope to "Vue 2 syntax on Vue 3 engine," ship that, and schedule the Composition API rewrite as a separate, ongoing effort.
The migration build is a real bridge. Use it. Set a removal date. Track the warnings. And don't try to redesign the codebase and change the engine in the same pull request.


