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.
Vue 2 configured everything on the global Vue object, which leaked between apps and tests. Vue 3 scopes configuration to an application instance:
// 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.
| 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.
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.vue.config.js settings to vite.config.js, rename env variables from VUE_APP_* to VITE_*, and replace process.env with import.meta.env.Vue.extend becomes defineComponent; class-component decorators are unsupported, so convert to <script setup>.@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:
// vite.config.js
export default defineConfig({
plugins: [vue({ template: { compilerOptions: { compatConfig: { MODE: 2 } } } })],
resolve: { alias: { vue: '@vue/compat' } }
})// 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.
<script setup> and the Composition API; convert mixins to composables while still on Vue 2.configureCompat to lock in each fix.@vue/compat, lint with eslint-plugin-vue's Vue 3 config, and ship.v-model on custom components changed prop and event names, which silently breaks forms.What is `@vue/compat`?
createApp and per-app configuration instead of the global Vue object.v-model names, removed filters and event bus methods, renamed lifecycle hooks and transition classes.@vue/compat runs your app on Vue 3 with warnings for deprecated features, enabling incremental migration.Next lesson: Capstone Project: Task Manager with Vue Router and Pinia — combine everything into a complete, tested application.