Hop, the vueleap frog, leaping from a lily pad marked 2 to a lily pad marked 3 2 3

Vue 3 migration report

shop-front (sample Vue 2 app) · 21 files scanned · 2026-10-12 12:00 UTC · vueleap 0.1.2

55
Errors
17
Warnings
3
Info
18
Auto fixable
19
Files affected
59
Effort score (large)

Minimal @vue/compat configuration

Run Vue 3 by default and keep only the Vue 2 behaviors this project still relies on. Remove each flag as you fix the matching findings.

// main.js (with 'vue' aliased to '@vue/compat')
import { configureCompat } from 'vue'

configureCompat({
  MODE: 3,
  COMPONENT_FUNCTIONAL: true,
  COMPONENT_V_MODEL: true,
  CONFIG_DEVTOOLS: true,
  CONFIG_IGNORED_ELEMENTS: true,
  CONFIG_KEY_CODES: true,
  CONFIG_PRODUCTION_TIP: true,
  CUSTOM_DIR: true,
  FILTERS: true,
  GLOBAL_DELETE: true,
  GLOBAL_MOUNT: true,
  GLOBAL_PROTOTYPE: true,
  GLOBAL_SET: true,
  INSTANCE_ATTRS_CLASS_STYLE: true,
  INSTANCE_CHILDREN: true,
  INSTANCE_DELETE: true,
  INSTANCE_DESTROY: true,
  INSTANCE_EVENT_EMITTER: true,
  INSTANCE_EVENT_HOOKS: true,
  INSTANCE_LISTENERS: true,
  INSTANCE_SCOPED_SLOTS: true,
  INSTANCE_SET: true,
  OPTIONS_BEFORE_DESTROY: true,
  OPTIONS_DATA_FN: true,
  OPTIONS_DESTROYED: true,
  RENDER_FUNCTION: true,
  TRANSITION_CLASSES: true,
  TRANSITION_GROUP_ROOT: true,
  V_ON_KEYCODE_MODIFIER: true,
  WATCH_ARRAY: true,
})

// vite.config.js
import vue from '@vitejs/plugin-vue'

export default {
  resolve: { alias: { vue: '@vue/compat' } },
  plugins: [
    vue({
      template: {
        compilerOptions: {
          compatConfig: {
            MODE: 3,
            COMPILER_FILTERS: true,
            COMPILER_INLINE_TEMPLATE: true,
            COMPILER_IS_ON_ELEMENT: true,
            COMPILER_V_BIND_OBJECT_ORDER: true,
            COMPILER_V_BIND_SYNC: true,
            COMPILER_V_IF_V_FOR_PRECEDENCE: true,
            COMPILER_V_ON_NATIVE: true,
          },
        },
      },
    }),
  ],
}

Findings

Errorglobal-apiGlobal Vue API moved to the app instance8

Vue.component, Vue.use, Vue.mixin and Vue.directive become app.component, app.use, app.mixin and app.directive. Vue.nextTick and Vue.observable become named imports (nextTick, reactive).

  • src/legacy/inline.js:3:1
    Vue.component is removed from the global API: use app.component().
    Vue.component('legacy-counter', {
  • src/main.js:16:1
    Vue.use is removed from the global API: use app.use().
    Vue.use(BootstrapVue)
  • src/main.js:17:1
    Vue.mixin is removed from the global API: use app.mixin().
    Vue.mixin(tracking)
  • src/main.js:18:1
    Vue.component is removed from the global API: use app.component().
    Vue.component('BaseButton', BaseButton)
  • src/main.js:19:1
    Vue.directive is removed from the global API: use app.directive().
    Vue.directive('tooltip', tooltip)
  • src/main.js:20:1
    Vue.directive is removed from the global API: use app.directive().
    Vue.directive('focus', {
  • src/router/index.js:5:1
    Vue.use is removed from the global API: use app.use().
    Vue.use(VueRouter)
  • src/store/index.js:4:1
    Vue.use is removed from the global API: use app.use().
    Vue.use(Vuex)
ErrorfiltersFilters are removed6

Filters ({{ price | currency }}, Vue.filter, the filters option) no longer exist. Replace them with method calls, computed properties or global properties.

Migration guide@vue/compat: COMPILER_FILTERS FILTERS
  • src/components/ProductList.vue:5:25
    Filter "capitalize" used in a template expression. Call a method or use a computed property instead.
    {{ product.name | capitalize }} costs {{ product.price | currency('EUR') }}
  • src/components/ProductList.vue:5:64
    Filter "currency" used in a template expression. Call a method or use a computed property instead.
    {{ product.name | capitalize }} costs {{ product.price | currency('EUR') }}
  • src/components/ProductList.vue:6:69
    Filter "capitalize" used in a template expression. Call a method or use a computed property instead.
    <product-card :product.sync="selected" :title="product.name | capitalize" />
  • src/components/ProductList.vue:30:5
    Local filter "capitalize" is removed: turn it into a method or a computed property.
    capitalize(value) {
  • src/legacy/inline.js:7:55
    Filter "number" used in a template expression. Call a method or use a computed property instead.
    template: '<button @click.native="count++">{{ count | number }}</button>',
  • src/main.js:26:1
    Vue.filter is removed: expose a helper through app.config.globalProperties or import a function.
    Vue.filter('currency', (value) => `$${Number(value).toFixed(2)}`)
Errorset-delete$set, $delete, Vue.set and Vue.delete are removed5

Vue 3 reactivity is based on Proxy, so plain assignment and the delete operator are tracked. Replace this.$set(obj, key, value) with obj[key] = value.

Migration guide@vue/compat: GLOBAL_DELETE GLOBAL_SET INSTANCE_DELETE INSTANCE_SET
  • src/components/ProductList.vue:55:12
    $set is removed: assign the property directly, Vue 3 tracks it.
    this.$set(product, 'inCart', true)
  • src/store/index.js:10:7
    Vue.set is removed: assign the property directly, Vue 3 tracks it.
    Vue.set(state.cart, product.id, product)
  • src/store/index.js:13:7
    Vue.delete is removed: use the delete operator, Vue 3 tracks it.
    Vue.delete(state.cart, id)
  • src/views/Home.vue:23:10
    $set is removed: assign the property directly, Vue 3 tracks it.
    this.$set(this.form, 'email', '')
  • src/views/Home.vue:24:10
    $delete is removed: use the delete operator, Vue 3 tracks it.
    this.$delete(this.form, 'legacy')
Errordirective-hooksCustom directive hooks are renamed4 auto fixable4

Directive hooks follow the component lifecycle: bind is beforeMount, inserted is mounted, componentUpdated is updated, unbind is unmounted, and update was removed.

Migration guide@vue/compat: CUSTOM_DIR
  • src/directives/tooltip.js:2:3 auto fix
    Directive hook bind is renamed to beforeMount.
    bind(el, binding) {
  • src/directives/tooltip.js:5:3 auto fix
    Directive hook componentUpdated is renamed to updated.
    componentUpdated(el, binding) {
  • src/directives/tooltip.js:8:3 auto fix
    Directive hook unbind is renamed to unmounted.
    unbind(el) {
  • src/main.js:21:3 auto fix
    Directive hook inserted is renamed to mounted.
    inserted(el) {
Errordestroy-hooksbeforeDestroy and destroyed are renamed3 auto fixable3

beforeDestroy is now beforeUnmount and destroyed is now unmounted. The old hooks are silently ignored.

Migration guide@vue/compat: OPTIONS_BEFORE_DESTROY OPTIONS_DESTROYED
  • src/components/Modal.vue:27:3 auto fix
    destroyed is renamed to unmounted and is ignored by Vue 3.
    destroyed() {
  • src/components/ProductList.vue:50:3 auto fix
    beforeDestroy is renamed to beforeUnmount and is ignored by Vue 3.
    beforeDestroy() {
  • src/mixins/tracking.js:5:3 auto fix
    beforeDestroy is renamed to beforeUnmount and is ignored by Vue 3.
    beforeDestroy() {
Errorkeycode-modifiersKeyCode modifiers are removed2 auto fixable3

Numeric modifiers such as @keyup.13 must use key names such as @keyup.enter.

Migration guide@vue/compat: V_ON_KEYCODE_MODIFIER
  • src/components/TheHeader.vue:5:55 auto fix
    KeyCode modifier .13 is removed: use .enter.
    <input v-model="query" placeholder="Search" @keyup.13="search" @keyup.27="query = ''" @keydown.112="help" />
  • src/components/TheHeader.vue:5:74 auto fix
    KeyCode modifier .27 is removed: use .esc.
    <input v-model="query" placeholder="Search" @keyup.13="search" @keyup.27="query = ''" @keydown.112="help" />
  • src/components/TheHeader.vue:5:99
    KeyCode modifier .112 is removed: use the kebab-case key name instead.
    <input v-model="query" placeholder="Search" @keyup.13="search" @keyup.27="query = ''" @keydown.112="help" />
Errorscoped-slots$scopedSlots is removed2 auto fixable2

All slots are exposed on $slots as functions. $scopedSlots.x becomes $slots.x.

Migration guide@vue/compat: INSTANCE_SCOPED_SLOTS
  • src/components/FancyList.js:7:46 auto fix
    $scopedSlots is removed: use $slots, which now exposes slots as functions.
    this.items.map((item) => h('li', [this.$scopedSlots.default({ item })])),
  • src/components/Modal.vue:7:16 auto fix
    $scopedSlots is removed: use $slots, which now exposes slots as functions.
    <div v-if="$scopedSlots.extra" class="extra">
Errorevent-emitter$on, $off and $once are removed2

Instances are no longer event emitters, so event buses built on new Vue() stop working. Use props and emits, provide/inject, a store, or a small library such as mitt.

Migration guide@vue/compat: INSTANCE_EVENT_EMITTER
  • src/components/ProductList.vue:47:14
    $on is removed: instances are no longer event emitters (use props and emits, provide/inject, a store, or mitt).
    EventBus.$on('cart:add', this.onAdd)
  • src/components/ProductList.vue:51:14
    $off is removed: instances are no longer event emitters (use props and emits, provide/inject, a store, or mitt).
    EventBus.$off('cart:add', this.onAdd)
Errorglobal-mountnew Vue() is replaced by createApp()2

Create the root with createApp(App) and mount it with app.mount('#app').

Migration guide@vue/compat: GLOBAL_MOUNT
  • src/event-bus.js:3:25
    new Vue() is removed: create the app with createApp(App) and call app.mount().
    export const EventBus = new Vue()
  • src/main.js:29:1
    new Vue() is removed: create the app with createApp(App) and call app.mount().
    new Vue({
Errorcomponent-v-modelv-model on components uses modelValue2

A component used with v-model now receives modelValue and must emit update:modelValue. The value prop, the input event and the model option no longer apply.

Migration guide@vue/compat: COMPONENT_V_MODEL
  • src/components/BaseInput.vue:12:11
    Prop "value" combined with $emit('input'): v-model on components now passes modelValue and listens to update:modelValue.
    props: ['value', 'label'],
  • src/components/CustomSelect.vue:10:3
    The model option is removed: v-model now uses the modelValue prop and the update:modelValue event (or v-model:name arguments).
    model: {
Errorrender-function-hRender functions no longer receive h2

Import h from vue instead of taking it as an argument. The VNode props shape is also flatter (on, attrs and domProps are gone).

Migration guide@vue/compat: RENDER_FUNCTION
  • src/components/FancyList.js:4:10
    render(h) no longer receives h: import { h } from 'vue' and update the VNode props format.
    render(h) {
  • src/main.js:32:12
    render(h) no longer receives h: import { h } from 'vue' and update the VNode props format.
    render: (h) => h(App),
Errordeprecated-slot-syntaxslot and slot-scope attributes are removed2

The slot, slot-scope and scope attributes were deprecated in Vue 2.6 and removed in Vue 3. Use v-slot (or #name).

  • src/components/LegacyWidget.vue:11:17
    The slot attribute is removed: use v-slot (or #name) on a <template>.
    <template slot="footer">
  • src/components/ProductList.vue:16:17
    slot-scope is removed: use v-slot="props" (or #name="props").
    <template slot-scope="{ item }">{{ item.name }}</template>
Errorlisteners$listeners is removed1

Event listeners are now part of $attrs. v-on="$listeners" can usually be dropped when v-bind="$attrs" is present.

Migration guide@vue/compat: INSTANCE_LISTENERS
  • src/components/BaseInput.vue:4:49
    $listeners is removed: listeners are now included in $attrs.
    <input :value="value" v-bind="$attrs" v-on="$listeners" @input="$emit('input', $event.target.value)" />
Errorchildren$children is removed1

Use template refs to reach child component instances.

Migration guide@vue/compat: INSTANCE_CHILDREN
  • src/components/Modal.vue:21:12
    $children is removed: use template refs to access child components.
    this.$children.forEach((child) => child.reset && child.reset())
Errordestroy$destroy is removed1

Components can no longer destroy themselves. Unmount the app with app.unmount() or remove the component with v-if.

Migration guide@vue/compat: INSTANCE_DESTROY
  • src/components/Modal.vue:22:12
    $destroy is removed: unmount the app or remove the component with v-if.
    this.$destroy()
Errorglobal-prototypeVue.prototype is replaced by app.config.globalProperties1

Move Vue.prototype.$http = http to app.config.globalProperties.$http = http, or use provide/inject.

Migration guide@vue/compat: GLOBAL_PROTOTYPE
  • src/main.js:27:1
    Vue.prototype is removed: use app.config.globalProperties or provide/inject.
    Vue.prototype.$api = api
Errorconfig-key-codesVue.config.keyCodes is removed1

Custom key code aliases are gone. Use kebab-case key names such as @keyup.page-down.

Migration guide@vue/compat: CONFIG_KEY_CODES
  • src/main.js:14:1
    Vue.config.keyCodes is removed: use kebab-case key names in v-on modifiers.
    Vue.config.keyCodes = { f2: 113 }
Errorsync-modifier.sync is replaced by v-model:prop1 auto fixable1

:title.sync="value" becomes v-model:title="value". The child keeps emitting update:title.

Migration guide@vue/compat: COMPILER_V_BIND_SYNC
  • src/components/ProductList.vue:6:23 auto fix
    .sync is removed: use v-model:product="selected" instead.
    <product-card :product.sync="selected" :title="product.name | capitalize" />
Errorfunctional-componentsFunctional components changed1

The functional attribute on <template> and the functional: true option are gone. Use a normal component (the performance gap is negligible) or a plain function.

Migration guide@vue/compat: COMPONENT_FUNCTIONAL
  • src/components/BaseButton.vue:1:11
    <template functional> is removed: use a regular component (or a plain function).
    <template functional>
Errorinline-templateinline-template is removed1

Move the inner markup into the component itself, or pass it through a default slot.

Migration guide@vue/compat: COMPILER_INLINE_TEMPLATE
  • src/components/LegacyWidget.vue:3:19
    inline-template is removed: move the markup into the component or pass it as a slot.
    <legacy-panel inline-template>
Errortemplate-v-for-keyKey must be on <template v-for>1

With <template v-for>, the key belongs on the <template> tag. Keys on its children are a compile error in Vue 3.

  • src/components/ProductList.vue:11:13
    Move this key to the parent <template v-for>: keys on children of <template v-for> are a compile error in Vue 3.
    <dt :key="spec.id">{{ spec.label }}</dt>
Errorv-if-v-forv-if now has higher priority than v-for1

On the same element, v-if is evaluated first, so it cannot use the v-for variable. Filter the list in a computed property or move v-if to a wrapper.

Migration guide@vue/compat: COMPILER_V_IF_V_FOR_PRECEDENCE
  • src/components/ProductList.vue:4:39
    v-if and v-for on the same element: in Vue 3 v-if runs first and cannot see the loop variable.
    <li v-for="product in products" v-if="product.visible" :key="product.id">
Errordata-objectdata must be a function1

Declaring data as a plain object is no longer allowed, including on the root instance. Use data() { return { ... } }.

Migration guide@vue/compat: OPTIONS_DATA_FN
  • src/components/LegacyWidget.vue:21:3
    data must be a function: use data() { return { ... } }.
    data: {
Errorvue-router-v3vue-router 3 does not support Vue 31

Upgrade to vue-router 4: new VueRouter() becomes createRouter(), mode becomes history: createWebHistory(), and catch-all routes need a custom regex.

  • package.json:13:5
    vue-router ^3.5.4 only works with Vue 2: upgrade to vue-router 4 (createRouter, createWebHistory).
    "vue-router": "^3.5.4",
Errorvuex-v3Vuex 3 does not support Vue 31

Upgrade to Vuex 4 (createStore), or move to Pinia, the official recommendation for new Vue 3 code.

  • package.json:14:5
    vuex ^3.6.2 only works with Vue 2: upgrade to Vuex 4 (createStore) or migrate to Pinia, the recommended store for Vue 3.
    "vuex": "^3.6.2"
Errorvue2-librariesVue 2 only library1

This library only supports Vue 2. Each finding names its Vue 3 successor.

  • package.json:10:5
    bootstrap-vue ^2.23.1: BootstrapVue only supports Vue 2. Migrate to bootstrap-vue-next (Bootstrap 5) or plain Bootstrap 5 markup.
    "bootstrap-vue": "^2.23.1",
Warningtransition-classesTransition classes renamed4 auto fixable4

.v-enter is now .v-enter-from and .v-leave is now .v-leave-from. The enter-class and leave-class props are now enter-from-class and leave-from-class.

Migration guide@vue/compat: TRANSITION_CLASSES
  • src/App.vue:34:1 auto fix
    .fade-enter is renamed to .fade-enter-from in Vue 3.
    .fade-enter,
  • src/components/Modal.vue:38:1 auto fix
    .list-enter is renamed to .list-enter-from in Vue 3.
    .list-enter,
  • src/styles/transitions.scss:2:1 auto fix
    .v-enter is renamed to .v-enter-from in Vue 3.
    .v-enter,
  • src/styles/transitions.scss:16:1 auto fix
    .slide-enter is renamed to .slide-enter-from in Vue 3.
    .slide-enter,
Warninghook-eventshook: lifecycle events are renamed1 auto fixable2

Listening to a child lifecycle with @hook:mounted becomes @vue:mounted. $on('hook:...') no longer works.

Migration guide@vue/compat: INSTANCE_EVENT_EMITTER INSTANCE_EVENT_HOOKS
  • src/components/Modal.vue:2:23 auto fix
    @hook:mounted is now @vue:mounted.
    <div class="modal" @hook:mounted="onChildMounted">
  • src/components/ProductList.vue:48:10
    $once('hook:beforeDestroy') no longer works: use the lifecycle hook directly, or @vue:beforeUnmount on the parent template.
    this.$once('hook:beforeDestroy', () => clearInterval(this.timer))
Warningnative-modifierThe .native modifier is removed2

Listeners not declared in the child emits option are added to its root element. Remove .native and declare the events the child really emits.

Migration guide@vue/compat: COMPILER_V_ON_NATIVE
  • src/App.vue:3:47
    .native is removed: declare the events the child emits with "emits", other listeners fall through to its root element.
    <the-header :cart-count="cartCount" @click.native="toggleMenu" />
  • src/legacy/inline.js:7:28
    .native is removed: declare the events the child emits with "emits", other listeners fall through to its root element.
    template: '<button @click.native="count++">{{ count | number }}</button>',
Warningv-bind-object-orderv-bind="object" is order sensitive2

Attributes declared before v-bind="obj" can now be overridden by the object. Move v-bind="obj" first if the explicit attributes should win, as they did in Vue 2.

Migration guide@vue/compat: COMPILER_V_BIND_OBJECT_ORDER
  • src/components/BaseInput.vue:4:27
    v-bind="$attrs" comes after value: in Vue 3 the object now overrides those attributes. Move it first to keep Vue 2 behavior.
    <input :value="value" v-bind="$attrs" v-on="$listeners" @input="$emit('input', $event.target.value)" />
  • src/components/LegacyWidget.vue:9:37
    v-bind="widgetAttrs" comes after id: in Vue 3 the object now overrides those attributes. Move it first to keep Vue 2 behavior.
    <div id="widget" class="widget" v-bind="widgetAttrs"></div>
Warningvue2-toolingVue 2 build or test tooling2

Compilers, loaders and test utilities are versioned with Vue. Each one has a Vue 3 replacement.

  • package.json:18:5
    @vue/test-utils ^1.3.6: @vue/test-utils 1 is for Vue 2: upgrade to @vue/test-utils 2.
    "@vue/test-utils": "^1.3.6",
  • package.json:19:5
    vue-template-compiler ^2.6.14: vue-template-compiler is Vue 2 only: remove it, Vue 3 ships @vue/compiler-sfc.
    "vue-template-compiler": "^2.6.14"
Warningconfig-production-tipVue.config.productionTip is removed1 auto fixable1

The option no longer exists. The statement can simply be deleted.

Migration guide@vue/compat: CONFIG_PRODUCTION_TIP
  • src/main.js:11:1 auto fix
    Vue.config.productionTip is removed: delete this line.
    Vue.config.productionTip = false
Warningconfig-ignored-elementsVue.config.ignoredElements is replaced by isCustomElement1

Use compilerOptions.isCustomElement in your build config (or app.config.compilerOptions.isCustomElement with the runtime compiler). It takes a function instead of strings or regular expressions.

Migration guide@vue/compat: CONFIG_IGNORED_ELEMENTS
  • src/main.js:13:1
    Vue.config.ignoredElements is removed: use compilerOptions.isCustomElement (a function).
    Vue.config.ignoredElements = ['ion-icon']
Warningglobal-configVue.config moved to app.config1

Global configuration now lives on app.config. Some options (silent, devtools) were removed.

Migration guide@vue/compat: CONFIG_DEVTOOLS
  • src/main.js:12:1
    Vue.config.devtools is not available in Vue 3: use app.config on the instance returned by createApp().
    Vue.config.devtools = true
Warningis-on-elementis on native elements needs the vue: prefix1

<tr is="my-row"> now creates a customized built-in element. Use is="vue:my-row" to render a Vue component.

Migration guide@vue/compat: COMPILER_IS_ON_ELEMENT
  • src/components/LegacyWidget.vue:7:11
    is="product-row" on <tr> creates a customized built-in element in Vue 3. Use is="vue:product-row" to render a component.
    <tr is="product-row" v-for="row in rows" :key="row.id" :row="row"></tr>
Warningvue-versionVue 2 dependency1

Upgrade vue to version 3. During the migration, alias vue to @vue/compat to run Vue 2 code on Vue 3.

  • package.json:12:5
    vue ^2.6.14 is Vue 2: upgrade to vue@^3.5. To migrate step by step, alias vue to @vue/compat first.
    "vue": "^2.6.14",
Infotransition-group-root<transition-group> renders no wrapper1

Without a tag prop, <transition-group> no longer renders a <span>. Add tag="span" if your CSS relies on it.

Migration guide@vue/compat: TRANSITION_GROUP_ROOT
  • src/components/Modal.vue:3:5
    <transition-group> without a tag prop no longer renders a wrapper <span>.
    <transition-group name="list">
Infoattrs-class-style$attrs now includes class and style1

Check components that forward $attrs, especially with inheritAttrs: false, because class and style now travel with them.

Migration guide@vue/compat: INSTANCE_ATTRS_CLASS_STYLE
  • src/components/BaseInput.vue:4:35
    $attrs now also contains class and style.
    <input :value="value" v-bind="$attrs" v-on="$listeners" @input="$emit('input', $event.target.value)" />
Infowatch-arrayArray watchers need deep for mutations1

Watching an array only fires when the array is replaced. Add deep: true if you rely on push, splice and friends.

Migration guide@vue/compat: WATCH_ARRAY
  • src/components/ProductList.vue:38:5
    "products" is an array: this watcher only fires when the array is replaced. Add deep: true to react to mutations.
    products(list) {