Migrating from Vue 2 to Vue 3

Advanced
13 min

Migrating from Vue 2 to Vue 3

Vue 2 reached end of life on December 31, 2023: no more security fixes, and modern versions of Vue Router, Vite and most libraries target Vue 3 only. After this lesson you will recognize every breaking change that affects typical applications, use the migration build to upgrade incrementally, and follow a plan that keeps the app shippable throughout.

Global API: from Vue to createApp

Vue 2 configured everything on the global Vue object, which leaked between apps and tests. Vue 3 scopes configuration to an application instance:

javascript
// Vue 2 import Vue from 'vue' Vue.component('BaseButton', BaseButton) Vue.directive('focus', focusDirective) Vue.prototype.$http = http new Vue({ router, store, render: h => h(App) }).$mount('#app') // Vue 3 import { createApp } from 'vue' const app = createApp(App) app.component('BaseButton', BaseButton) app.directive('focus', focusDirective) app.config.globalProperties.$http = http app.use(router).use(pinia) app.mount('#app')

One subtle difference: Vue 2 replaced the mount element with the root component's element; Vue 3 renders inside it, so <div id="app"> remains in the DOM.

Template and Component Breaking Changes

| Vue 2 | Vue 3 | | --- | --- | | v-model on components uses value / input | modelValue / update:modelValue; multiple v-model:name | | .sync modifier | Replaced by v-model:prop | | Template filters (pipe syntax) | Removed; use methods, computed or global properties | | $listeners | Merged into $attrs | | key on <template v-for> children | key goes on the <template> itself | | v-if / v-for on the same element | v-if now has higher priority; avoid combining them | | beforeDestroy / destroyed | beforeUnmount / unmounted | | Transition class v-enter / v-leave | v-enter-from / v-leave-from | | $children, $on, $off, $once | Removed; use refs, provide/inject, or an emitter library such as mitt | | Async component () => import() | defineAsyncComponent(() => import()) | | Vue.set / Vue.delete | Unnecessary; Proxy reactivity tracks added and deleted properties |

Components may now have multiple root nodes, the emits option declares events, and data must be a function. Proxy reactivity tracks array changes and added properties without helpers, but reactive(obj) !== obj.

Ecosystem Upgrades

  • Vue Router 3 to 4: new VueRouter({ mode: 'history' }) becomes createRouter({ history: createWebHistory() }); the catch-all * becomes /:pathMatch(.*)*; guards return values instead of calling next(); router.match is router.resolve.
  • Vuex to Pinia: Vuex 4 works with Vue 3 as a bridge, but Pinia is the official store. Mutations disappear; actions mutate state directly. Migrate one module at a time by mounting both.
  • Vue CLI to Vite: Vue CLI is in maintenance mode. Move vue.config.js settings to vite.config.js, rename env variables from VUE_APP_* to VITE_*, and replace process.env with import.meta.env.
  • TypeScript: Vue.extend becomes defineComponent; class-component decorators are unsupported, so convert to <script setup>.

The Migration Build

@vue/compat is a build of Vue 3 that behaves like Vue 2 by default and logs a warning for every deprecated feature it encounters. It lets a large app run on Vue 3 immediately while you fix warnings incrementally:

javascript
// vite.config.js export default defineConfig({ plugins: [vue({ template: { compilerOptions: { compatConfig: { MODE: 2 } } } })], resolve: { alias: { vue: '@vue/compat' } } })
javascript
// main.js: opt individual features into Vue 3 behaviour as you fix them import { configureCompat } from 'vue' configureCompat({ COMPONENT_V_MODEL: false, INSTANCE_LISTENERS: false })

Once every warning is resolved, remove the alias and compatConfig. Vue 2 plugins that rely on removed internals are not supported by the migration build and must be replaced first.

A Step-by-Step Plan

  1. Upgrade to the latest Vue 2.7, which backports <script setup> and the Composition API; convert mixins to composables while still on Vue 2.
  2. Replace unsupported dependencies and the event bus.
  3. Switch to Vite and the migration build; get the test suite green.
  4. Fix compat warnings feature by feature, using configureCompat to lock in each fix.
  5. Move Vuex to Pinia and Vue Router to version 4.
  6. Remove @vue/compat, lint with eslint-plugin-vue's Vue 3 config, and ship.

Common mistakes

  • Migrating everything in one branch; keep the app deployable between steps.
  • Forgetting that v-model on custom components changed prop and event names, which silently breaks forms.
  • Leaving filters in templates; they compile to errors in Vue 3.
Quick Quiz
Question 1 of 2

What is `@vue/compat`?

Key Takeaways

  • Vue 2 is end of life; Vue 3 uses createApp and per-app configuration instead of the global Vue object.
  • Key breaking changes: component v-model names, removed filters and event bus methods, renamed lifecycle hooks and transition classes.
  • Upgrade the ecosystem alongside: Vue Router 4, Pinia instead of Vuex, Vite instead of Vue CLI.
  • @vue/compat runs your app on Vue 3 with warnings for deprecated features, enabling incremental migration.
  • Start from Vue 2.7, migrate step by step, and keep the app deployable at every stage.

Next lesson: Capstone Project: Task Manager with Vue Router and Pinia — combine everything into a complete, tested application.

Migrating from Vue 2 to Vue 3 - Vue.js | CodeYourCraft | CodeYourCraft