What are the real benefits and risks of vibe coding?
Vibe coding lets you build software fast with AI, no code required. But is it ready for production? Here’s what you need to know.

Tiago Coelho
CTO

The Vue.js team recently announced that Vue 2 will reach its end of life by 2023. And that’s the perfect reason to start thinking about migrating your Vue 2 application to Vue 3.
The process of upgrading an app to the latest version of the framework can be a daunting task. But gladly for us, since June 2021, the Vue.js Team released a Migration Build to make the migration process easier.
This article will focus on our experience migrating a large-scale application from Vue 2 to Vue 3.
The answer is yes. As mentioned before, Vue 2.0 will reach its EOL at the end of 2023, so you should start planning this. And there are more reasons to migrate, such as:
Faster rendering. Benchmarks mention that the initial render is up to 55% faster, updates are up to 133% faster, and memory usage has been lowered to 54% less.
Improved TypeScript. Its codebase was entirely written in TypeScript, with auto-generated type definitions. You'll have pretty neat stuff like type inference and props type checking right within templates.
You can now use Composition API. You might hate it first if you're too used to building components with the Options API, but the Composition API will improve your Developer Experience once you get used to it. Nevertheless, if you really want to, you can still keep building with the Options API, which is still 100% supported.
We’ve experienced this migration on a large-scale application that millions of users use. And we also had another ongoing scope around the application while we did the transition — we worked on new features and bug fixes in parallel. The project’s new features couldn’t stop until we completed the migration.
To optimize both works at the same time:
All the work that included new features, product support, and even tech debt, was still worked on our Vue 2 application;
All of the work for our Vue 3 migration was done on a separate branch with multiple sub-branches.
For every change we pushed to the main branch, we had to sync our Vue 3 branch and apply whatever necessary changes needed to make it Vue 3 compatible.
Having a successful migration meant not breaking anything. So, to ensure that everything was ready to finish the transition, we made the application go through a full regression test to be sure every feature fully worked.
We can achieve a complete migration within six steps:
Install Vue's Migration Build;
Fix the Migration Build's Errors;
Fix the Migration Build's Warnings;
Fix package compatibilities;
Fix TypeScript support;
Fully Switch to Vue 3.
As mentioned before, the Vue Team built a package called @vue/compat, also known as Migration Build, that allows your application to support both Vue 2 and Vue 3. This package isn’t meant to be used in production because it has many degraded performances.It’s only supposed to be used while converting the application. After finishing the whole upgrade, you should remove it.
First, upgrade tooling if applicable:
If using a custom Webpack setup: Upgrade vue-loader to ^16.0.0.
If using vue-cli: upgrade to the latest @vue/cli-service with vue upgrade.
Secondly, modify the package.json to install the following packages:
Update vue to its latest version;
Install @vue/compat in the same version as vue;
Replace vue-template-compiler (if present) with @vue/compiler-sfc in the same version as vue.
1// package.json23{4 // ...56 "dependencies": {7 "vue": "^3.2.0", // UPDATE8 "@vue/compat": "^3.2.0" // ADD9 // ...10 },11 "devDependencies": {12 "@vue/compiler-sfc": "^3.2.0" // ADD13 "vue-template-compiler": "^2.6.0" // REMOVE14 // ...15 }16}
Then, you will need to enable the Migration Build's compatibility mode.
If you're using Webpack, add the following config to your webpack.config.js:GIST HERE target="_blank" id="">
1// webpack.config.js23module.exports = {45 // ...67 resolve: {8 alias: {9 vue: '@vue/compat'10 }11 },12 module: {13 rules: [14 {15 test: /\.vue$/,16 loader: 'vue-loader',17 options: {18 compilerOptions: {19 compatConfig: {20 // Default everything to Vue 2 behavior21 MODE: 222 }23 }24 }25 }26 ]27 }28}
If you're using the Vue CLI, Vite, or some other build systems, check out vue-compat’s instructions on how to enable it.
Hooray! The Migration Build is all set and done.
Although, if you try to run your application, there's a high chance that it still won't work — Migration Build isn't 100% compatible with Vue 3. There are a few deprecations that you need to fix first.
Below, there’s a list of errors that don't allow your app to run without fixing them first.
The syntax for using name slots and using scoped slots has changed.For slots in Vue 3, the v-slot directive, or its # shorthand as an alternative, should be used instead to identify the slot name with the slot's modifier and pass props to it.
In Vue 2:
1<template>2 <CustomDialog>3 <template slot="heading" slot-scope="slotProps">4 <h1>Items ({{ slotProps.items.length }})</h1>5 </template>6 </CustomDialog>7</template>
In Vue 3:
1<template>2 <CustomDialog>3 <template #heading="slotProps">4 <h1>Items ({{ slotProps.items.length }})</h1>5 </template>6 </CustomDialog>7</template>
The functional attribute was deprecated. The performance advantages in having functional components in Vue 2 vs. Vue 3 have been drastically reduced almost to insignificance.
The suggested migration for this scenario is simply removing the attribute, and everything should work the same.
In Vue 2:
1<template functional>2 <h1>{{ title }}</h1>3</template>45<script>6export default {7 props: {8 title: {9 type: String,10 required: true,11 },12 }13}14</script>
In Vue 3:
1<template>2 <h1>{{ title }}</h1>3</template>45<script>6export default {7 props: {8 title: {9 type: String,10 required: true,11 },12 }13}14</script>
Vue doesn't replace the element where your app is mounted anymore, so you'll need to be careful with the name given to the id. Otherwise, you can find your application's rendering coming out like this:
If you find this happening on your application, simply rename the id of one of the two div's to a different name.
For performance reasons, using v-if conditions in the same element that's using v-for no longer works in Vue 3.
Alternatively, you can wrap the element with a <template> and add the v-if conditional.
In Vue 2:
1<template>2 <div3 v-if="list"4 v-for="item in list"5 :key="item.id"6 :title="item.title">7 {{ item.title }}8 </div>9</template>
In Vue 3:
1<template>2 <template v-if="list">3 <div4 v-for="item in list"5 :key="item.id"6 :title="item.title">7 {{ item.title }}8 </div>9 </template>10</template>
You can no longer have the same key for multiple branches of the same v-if conditional.
To fix this, we need to either assign different key names to them or remove the key (don't worry, Vue will assign unique keys to them automatically).
In Vue 2:
1<template>2 <ul>3 <li v-for="item in list">4 <p v-if="item.amount < 10" :key="item.id">{{ item.title }}</p>5 <p v-else class="high" :key="item.id">{{ item.title }}</p>6 </li>7 </ul>8</template>
In Vue 3:
1<template>2 <ul>3 <li v-for="item in list">4 <p v-if="item.amount < 10">{{ item.title }}</p>5 <p v-else class="high">{{ item.title }}</p>6 </li>7 </ul>8</template>
In Vue 3, when using v-for on a <template>, it is now invalid to use the :key in inner elements.
To fix this, pass the :key to the <template> instead.
In Vue 2:
1<template>2 <template v-for="item in list">3 <div :key="item.id">{{ item.title }}</div>4 </template>5</template>
In Vue 3:
1<template>2 <template v-for="item in list" :key="item.id">3 <div>{{ item.title }}</div>4 </template>5</template>
When using the <transition> element for animation purposes, the class names v-enter and v-leave were renamed to v-enter-from and v-leave-from, respectively.
If you're using these classes in your application, update their names, and everything should continue to work accordingly.
In Vue 2:
1<style lang="scss">2 .v-enter,3 .v-leave-to {4 opacity: 0;5 }67 .v-leave,8 .v-enter-to {9 opacity: 1;10 }11</style>
In Vue 3:
1<style lang="scss">2 .v-enter-from,3 .v-leave-to {4 opacity: 0;5 }67 .v-leave-from,8 .v-enter-to {9 opacity: 1;10 }11</style>
If you have reached this far, there's a good chance that you can now run your app! But don't party yet… there's still work to be done.
If you run your application, you'll find console warnings similar to this.
Despite the warnings, your app works just fine on the Migration Build. Although, these warnings let you know what you need to fix before fully switching from the Migration Build to Vue 3.
Each warning has an identifier (e.g., GLOBAL_SET, OPTIONS_DESTROYED, GLOBAL_PROTOTYPE, etc.). You can find a complete list of all the different warnings here.
But, instead of opening all the pages of your app to collect all the warnings being fired, we combined a list of what we consider to be the most common errors present on a large application.
Let's continue with our migration!
Starting Vue 3, your app and plugins are no longer instantiated globally. You can now have multiple Vue apps within the same project.
Vue's way of initializing an app is now different.
Instead of initializing it with new Vue and $mount, use createApp and mount instead.
In Vue 2:
1import Vue from 'vue'2import App from 'views/app/app.vue'34new Vue({5 router,6 render: h => h(App)7}).$mount('#app')
In Vue 3:
1import { createApp } from 'vue'2import App from 'views/app/app.vue'34const app = createApp(App)5app.mount('#app')
In Vue 3, there is no longer a Vue import from the vue package. Instead, we need to specify the Vue app from where we want to call our methods.
Here's a list of the changes from the old Global API to the new Instance API.
Vue.component ⇒ app.component
Vue.use ⇒ app.use
Vue.config ⇒ app.config
Vue.directive ⇒ app.directive
Vue.mixin ⇒ app.mixin
All you need to do for all these cases is replace Vue with the app.
In Vue 2:
1import somePlugin from 'plugins/some-plugin'2import someDirective from 'directives/some-directive'34Vue.use(somePlugin)5Vue.directive('some-directive', someDirective)
In Vue 3:
1import somePlugin from 'plugins/some-plugin'2import someDirective from 'directives/some-directive'34app.use(somePlugin)5app.directive('some-directive', someDirective)
Both Vue.extend and Vue.util were removed and can no longer be used. If you're using them in your application, you should simply remove them.
Note: Instead of removing Vue.extend, this is a good opportunity to replace it with defineComponent, a type helper function that defines a Vue Component with type inference. With it, you'll have all the types of information about the component when you get the chance to use it.
In Vue 2:
1<script lang="ts">2 import Vue from 'vue'3 export default Vue.extend({4 //5 })6</script>
In Vue 3:
1<script lang="ts">2 import { defineComponent } from 'vue'34 export default defineComponent({5 //6 })7</script>
Once again, since you can now have multiple apps on your project, you now need to specify which app you want to assign your global properties.
Instead of Vue.prototype, use app.config.globalProperties.
In Vue 2:
1Vue.prototype.$title = 'Some title'
In Vue 3:
1app.config.globalProperties.$title = 'Some title'
APIs like Vue.set or Vue.delete are now deprecated.
To to fix this, instead of using Vue.set(object, key, value) you can directly use object[key] = value.
The same goes for Vue.delete(object, key), which can be replaced with delete object[key].
In Vue 2:
1// set2Vue.set(state.moduleName, 'keyName', 'value')3vm.$set(state.moduleName, 'keyName', 'value')45// delete6Vue.delete(state.moduleName, 'keyName', 'value')7vm.$delete(state.moduleName, 'keyName', 'value')
In Vue 3:
1// set2Object.assign(state.moduleName, { keyName: 'value' })34// delete5delete state.moduleName.keyName
In Vue 3, nextTick is no longer part of the Global API and can now only be accessed as a named export from vue.
To get around this, you'll need to replace it like in the example below.
In Vue 2:
1<script lang="ts">2 this.$nextTick(() => {3 // something DOM-related4 })5</script>
In Vue 3:
1<script lang="ts">2 import { nextTick } from 'vue'3 nextTick(() => {4 // something DOM-related5 })6</script>
The lifecycle hooks beforeDestroy and destroyed were renamed. You will have to update them in your application to beforeUnmount and unmounted, respectively.
In Vue 2:
1<script lang="ts">2 export default {3 beforeDestroy() {4 console.log('Before component unmounts!')5 },6 destroyed() {7 console.log('Component unmounted!')8 },9 }10</script>
In Vue 3:
1<script lang="ts">2 export default {3 beforeUnmount() {4 console.log('Before component unmounts!')5 },6 unmounted() {7 console.log('Component unmounted!')8 },9 }10</script>
All events must be declared via the new emit option (similar to the existing props option).
For instance, if your component has a @click property that's emitting the event using this.$emit('click'), you will have to declare the click event in your component.
1<script lang="ts">2 export default {3 emits: ['click'],4 methods: {5 onTitleClick(title) {6 this.$emit('click', title)7 }8 }9 }10</script>
In Vue 3, the attribute's prefix for listening lifecycle events in its parent component was changed from @hook to @vnode-.
Note: Don't forget to update the lifecycle hooks' names.
In Vue 2:
1<template>2 <MyComponent @hook:mounted="foo">3</template>
In Vue 3:
1<template>2 <MyComponent @vnode-mounted="foo">3</template>
The v-model API was changed in Vue 3. Before, the v-model targeted the property value, but now it's targeting model-value.
Taking the example of the component below, there are two different ways to fix this incompatibility.
1<template>2 <CustomComponent v-model="title" />3</template>
-->a) Specifying value as the prop to target for the two-way binding.
1<template>2 <CustomComponent v-model:value="title" />3</template>
-->b) Or alternatively, changing the prop name in the component from value to modelValue.
1<script lang="ts">2 export default {3 props: {4 modelValue: String // previously was `value: String`5 },6 emits: ['update:modelValue'],7 methods: {8 changeTitle(title) {9 this.$emit('update:modelValue', title)10 }11 }12 }13</script>
Since value was changed to modelValue, the event's name to mutate it was also changed from input to $emit('update:modelValue').
In case you’re calling $emit('input') in your application, you should replace it with $emit('update:modelValue').
In Vue 2:
1<script lang="ts">2 export default {3 methods: {4 updateTitle() {5 this.$emit('input')6 }7 }8 }9</script>
In Vue 3:
1<script lang="ts">2 export default {3 props: {4 modelValue: String // previously was `value: String`5 },6 emits: ['update:modelValue'],7 methods: {8 changeTitle(title) {9 this.$emit('update:modelValue', title)10 }11 }12 }13</script>
v-bind.sync was deprecated. This is often used for adding “two-way binding” to props.
Gladly for us, Vue 3 allows our components to have multiple v-model's by passing an argument for specifying the name of the prop we want the “two-way binding” to be added to.
All you need to do is replace :propName.sync with v-model:propName.
In Vue 2:
1<template>2 <ChildComponent v-model="active" :title.sync="pageTitle" />3</template>
In Vue 3:
1<template>2 <ChildComponent v-model="active" v-model:title="pageTitle" />3</template>4
In Vue 3, the hook functions for directives have been renamed to better align with the component lifecycle. Here’s a full list of the changes:
created. This is a new hook. It’s called before the element's attributes, or event listeners are applied.
bind → beforeMount. bind was renamed to beforeUnmount.
inserted → mounted. inserted was renamed to mounted.
beforeUpdate. This is a new hook. It’s called before the element itself is updated, just like in the component lifecycle hooks.
update. This hook was removed. There were too many similarities to updated, so it got considered redundant. You should replace it with updated instead.
componentUpdated → updated. componentUpdated was renamed to updated.
beforeUnmount. This is a new hook. It is called right before an element is unmounted.
unbind -> unmounted. unbind was renamed to unmounted.
To sum up, this is how the final API looks like:
1const MyDirective = {2 created(el, binding, vnode, prevVnode) {},3 beforeMount() {},4 mounted() {},5 beforeUpdate() {},6 updated() {},7 beforeUnmount() {},8 unmounted() {}9}
Another change in Custom Directives is the way to access the component instance.
If you're doing this in the application with vnode.context, you should update it with binding.instance.
In Vue 2:
1bind(el, binding, vnode) {2 const vm = vnode.context3}
In Vue 3:
1mounted(el, binding, vnode) {2 const vm = binding.instance3}
Filters are no longer supported in Vue 3.
If you're using filters in your application, you'll need to rewrite them as a computed property or a method.
In Vue 2:
1<template>2 <p>{{ user.lastName | uppercase }}</p>3</template>
In Vue 3:
1<template>2 <p>{{ uppercasedLastName }}</p>3<template>45<script lang="ts">6 export default {7 computed: {8 uppercasedLastName(): string {9 return this.user.lastName.toUpperCase()10 }11 },12 // or alternatively13 // {{ uppercaseText(user.lastName) }}14 methods: {15 uppercaseText(text: string) {16 return text.toUpperCase()17 }18 }19 }20</script>
This atribute is often used in native elements for having these elements as placeholders.
This is no longer possible in Vue 3. To fix this, you need to replace the native element with <component>.
In Vue 2:
1<template>2 <button is="someComponent" />3</template>
In Vue 3:
1<template>2 <component is="someComponent" />3</template>
vm.$on, vm.$off, vm.$once are deprecated
These global functions from Vue's instance, used to create a global EventBus, were removed in Vue 3.
To get the same result, Vue suggests installing a library called Mitt.
After installing it, you'll need to add Mitt's instantiation to your app's global properties.
1// e.g. in app.ts2import mitt from 'mitt'34const emitter = mitt()5app.config.globalProperties.$emitter = emitter
After that, the last step is to replace all the $on with $emitter.on.
In Vue 2:
1this.$on('clickAway', this.hidePopover)
In Vue 3:
1this.$emitter.on('clickAway', this.hidePopover)
The .native modifier for events is now deprecated.
By default, in Vue 3, all events attached to a component will be treated as native events and added to that component's root element.
To go around this, remove the .native modifier.
In Vue 2:
1<template>2 <SpecialButton v-on:click.native="foo" />3</template>
In Vue 3:
1<template>2 <SpecialButton v-on:click="foo" />3</template>
The $children instance property has been removed from Vue 3.
Alternatively, you could still access a child component instance using template refs.
In Vue 2:
1<template>2 <CustomComponent>Hello World</CustomComponent>3</template>45<script lang="ts">6 export default {7 mounted() {8 console.log(this.$children[0])9 },10 }11</script>
In Vue 3:
1<template>2 <CustomComponent ref="greeting">Hello World</CustomComponent>3</template>45<script lang="ts">6 export default {7 mounted() {8 console.log(this.$refs.greeting)9 },10 }11</script>
The $listeners object used to access the event handlers passed from the parent component has been removed in Vue 3.
If you still need to access these events, you have to access them individually in $attrs.
Note: The events in $attrs are prefixed with on.
In Vue 2:
1<script lang="ts">2 export default {3 mounted() {4 console.log(this.$listeners)5 }6 }7</script>
In Vue 3:
1<script lang="ts">2 export default {3 mounted() {4 console.log(this.$attrs.onClick)5 console.log(this.$attrs.onMouseenter)6 }7 }8</script>
When watching an array, the callback will only trigger when the array is fully replaced.
If you need to trigger every mutation within the array, you will need to specify deep: true in your watcher.
1<script lang="ts">2 export default {3 watch: {4 someList: {5 deep: true,6 handler(val, oldVal) {7 console.log('list was updated')8 },9 },10 }11 }12</script>
In Vue 3, the way to define an async component has changed.
You now need to do it using the new defineAsyncComponent function.
Note: The option component was also renamed to loader.
In Vue 2:
1const MyComponent = {2 component: () => import('./MyComponent.vue'),3 // ...4}
In Vue 3:
1import { defineAsyncComponent } from 'vue'23const MyComponent = defineAsyncComponent({4 loader: () => import('./MyComponent.vue'),5 // ...6})
In Vue 2, setting false to the value of an attribute would remove the attribute from being rendered. When doing this in Vue 3, false is considered a valid value, so the attribute will be rendered.
If you want to keep this from happening in your application, you should pass null to the attribute's value instead.
In Vue 2:
1<template>2 <p :alt="false">Hello</p>3</template>
In Vue 3:
1<template>2 <p :alt="null">Hello</p>3</template>
Vue itself isn't the only thing that needs a migration to make it compatible.
Most of the Vue packages (official and unofficial) had to release a new version to make them compatible. This means that now you'll need to upgrade them too… and be careful with breaking changes.
Gladly for us, all the official Vue libraries (e.g., Vuex, Vue Router) have been ported to Vue 3. The majority of unofficial packages got Vue 3 compatible versions, too.
But, once again, even though these packages might have compatible versions, you will still have to work on some changes in your app after upgrading them.
Initialization changed
To align with the new Vue 3 initialization process, the installation process of Vuex has changed. Users are now encouraged to create a new store using the newly introduced createStore function.
In Vue 2:
1export default new Vuex.Store<IRootState>(storeOptions)
In Vue 3:
1export default createStore(storeOptions)
Initialization changed
The installation process of Vue Router has also changed. Instead of writing new Router(), you now have to call createRouter.
In Vue 2:
1import Router from 'vue-router'23const router = new Router({4 // ...5})
In Vue 3:
1import { createRouter } from 'vue-router'23const router = createRouter({4 // ...5})
New history option to replace mode
1import { createRouter, createWebHistory } from 'vue-router'2// there is also createWebHashHistory and createMemoryHistory34createRouter({5 history: createWebHistory(),6 routes: [],7})
The prop tag is deprecated from RouterLink
This prop was often useful in moments when using the RouterLink, to not render an <a/> tag for a particular reason. Alternatively to that, you can go around it using a v-if/else conditional for rendering different HTML.
In Vue 2:
1<template>2 <router-link3 :tag="info.id ? 'a' : 'div'"4 :to="info.id ? { name: 'article', params: { id: info.id } } : {}"5 >6 {{ info.title }}7 </router-link>8<template>
In Vue 3:
1<template>2 <router-link3 v-if="info.id"4 :to="{ name: 'article', params: { id: info.id } }"5 >6 {{ info.title }}7 </router-link>8 <div v-else>{{ info.title }}</div>9</template>
Typescript Support
Vuex 4 removed its global typings for this.$store within a Vue component. When used with TypeScript, you must declare your own module augmentation.
To do so, create a file called vuex-shims.d.ts on the root of your project and place the following code to allow this.$store to be typed correctly.
1// vuex-shim.d.ts23import { ComponentCustomProperties } from 'vue'4import { Store } from 'vuex'56// Your own store state7interface IMyState {8 count: number9}1011declare module '@vue/runtime-core' {12 interface ComponentCustomProperties {13 $store: Store14 }15}
Previously, we mentioned that most popular packages already have a Vue 3 compatible version. But there are definitely packages around there that don't.
Every case is a case, and you will need to go through each vue-related package you have installed and check their GitHub documentation to know if they have a new Vue 3 compatible version.
If they do… good! Just upgrade it, and make whatever changes if that upgrade brings breaking changes.
If they don't, you can either:
Fork the library and make it compatible yourself;
Or find an alternative library compatible with Vue 3 that will do the same thing you need.
■ UI Libraries
If your application uses a UI Library (e.g., ElementUI, Vuetify, ChakraUI), you'll most likely find a lot of hard work in upgrading it.
In our experience, we had to migrate ElementUI to ElementPlus, and there were a lot of changes. The props names in the libraries components were modified, and the rendered HTML and class names got also changed, which affected our custom styling for those components.
Besides that, there was little or no documentation to help us on this migration, so our team manually detected every bug. And it was definitely the challenging part of the entire Vue 3 migration.
The good news is that ElementPlus now has documentation about what changed in their components and even made a CLI Tool called gogocode-cli that will automatically update your code with the new ElementPlus changes.
Here are some useful links in case you are migrating ElementPlus:
Documentation: https://github.com/element-plus/element-plus/discussions/5658
CLI Tool: https://github.com/thx/gogocode/tree/main/packages/gogocode-plugin-element
If you're using a different UI Library, you'll need to check their documentation and what needs to be done to upgrade it.
To make your .vue's TypeScript declaration files work, you'll need to make this change:
In Vue 2:
1declare module '*.vue' {2 import Vue from 'vue'3 export default Vue4}
In Vue 3:
1declare module '*.vue' {2 import { defineComponent } from 'vue'3 const component: ReturnType4 export default component5}
If, in Vue 2, you were augmenting Global Properties types, then you need to update it like this:
In Vue 2:
1declare module 'vue/types/vue' {2 export interface Vue {3 $http: typeof axios4 }5}
In Vue 3:
1declare module '@vue/runtime-core' {2 interface ComponentCustomProperties {3 $http: typeof axios4 }5}
This is the best part. When you get to the point where you think your application is fully working as intended and without any errors or warnings being thrown in your console, you’re ready to eliminate the migration Build!
All you need to do is:
Uninstall @vue/compat package;
Remove the changes we made at the beginning of the article to webpack.config.js (or other config files that you have). You won't need that alias or the vue-loader anymore.
And that's it! You successfully migrated your application to Vue 3! 🎉
Migrating from Vue 2 to Vue 3 might sound challenging, but it isn’t impossible. We hope our experience can help you start planning and implementing your migration. It will require time, effort, and learning, but it will help you keep your application as modern as possible.
Share this article