How to migrate from Vue 2 to Vue 3

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.

Should I really migrate 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:

  1. 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.
  2. 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.
  3. 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.

Workflow Strategy

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:

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.

Migration strategy

We can achieve a complete migration within six steps:

  1. Install Vue's Migration Build;
  2. Fix the Migration Build's Errors;
  3. Fix the Migration Build's Warnings;
  4. Fix package compatibilities;
  5. Fix TypeScript support;
  6. Fully Switch to Vue 3.

1. Install Vue's Migration Build

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:

Secondly, modify the package.json to install the following packages:

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:

If you're using the Vue CLI, Vite, or some other build systems, check out vue-compat’s instructions on how to enable it.

2. Fix the Migration Build's errors

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.

Named and Scoped Slots

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:

In Vue 3:

Functional attribute

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:

In Vue 3:

Mounted Container

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.

v-if combined with v-for

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:

In Vue 3:

v-if branch keys

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:

In Vue 3:

v-for keys in Templates

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:

In Vue 3:

Transition classes were renamed

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:

In Vue 3:

3. Fix the Migration Build's warnings

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!

App's Initialization

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:

In Vue 3:

Vue Global API

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.

All you need to do for all these cases is replace Vue with the app.

In Vue 2:

In Vue 3:

Vue.extend and Vue.util are deprecated

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:

In Vue 3:

Adding Global Instance Properties

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:

In Vue 3:

Reactive property setters

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:

In Vue 3:

$nextTick is no longer in the Global API

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:

In Vue 3:

Lifecycle Hooks

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:

In Vue 3:

All events need to be declared

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.

Lifecycle Events

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:

In Vue 3:

v-model prop

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.

a) Specifying value as the prop to target for the two-way binding.

b) Or alternatively, changing the prop name in the component from value to modelValue.

v-model event

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:

In Vue 3:

Syncing a prop

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:

In Vue 3:

Custom Directives - Changes in Hooks

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:

To sum up, this is how the final API looks like:

Custom Directives - Accessing the Component Instance

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:

In Vue 3:

Filters

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:

In Vue 3:

"is" attribute no longer works with native elements

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:

In Vue 3:

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.

After that, the last step is to replace all the $on with $emitter.on.

In Vue 2:

In Vue 3:

native event modifier is deprecated

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:

In Vue 3:

.$children is deprecated

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:

In Vue 3:

vm.$listeners is deprecated

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:

In Vue 3:

Watching Arrays

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.

Async Components

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:

In Vue 3:

Removing attributes

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:

In Vue 3:

4. Fix package compatibilities

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.

Official Vue Libraries

Vuex

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:

In Vue 3:

Vue Router

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:

In Vue 3:

New history option to replace mode

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:

In Vue 3:

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.


// vuex-shim.d.ts

import { ComponentCustomProperties } from 'vue'
import { Store } from 'vuex'

// Your own store state
interface IMyState {
  count: number
}

declare module '@vue/runtime-core' {
  interface ComponentCustomProperties {
    $store: Store
  }
}

Unofficial Vue Libraries

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:

■ 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:

If you're using a different UI Library, you'll need to check their documentation and what needs to be done to upgrade it.

5. Fix TypeScript support

To make your .vue's TypeScript declaration files work, you'll need to make this change:

In Vue 2:


declare module '*.vue' {
  import Vue from 'vue'
  export default Vue
}

In Vue 3:


declare module '*.vue' {
  import { defineComponent } from 'vue'
  const component: ReturnType
  export default component
}

Augmenting Global Properties

If, in Vue 2, you were augmenting Global Properties types, then you need to update it like this:

In Vue 2:


declare module 'vue/types/vue' {
  export interface Vue {
    $http: typeof axios
  }
}

In Vue 3:


declare module '@vue/runtime-core' {
  interface ComponentCustomProperties {
    $http: typeof axios
  }
}

6. Fully Switch to Vue 3

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:

And that's it! You successfully migrated your application to Vue 3! 🎉

Conclusion

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.

Sources

https://www.vuemastery.com/blog/migration/

https://crisp.chat/blog/vuejs-migration/

https://v3-migration.vuejs.org/

João Gonçalves
Front-End Developer