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:
- 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.
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:
- 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.
Migration strategy
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.
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:
- 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.
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.
- 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:
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:
- 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:
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:
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:
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.
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:
- 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.
5. Fix TypeScript support
To make your .vue's TypeScript declaration files work, you'll need to make this change:
In Vue 2:
Augmenting Global Properties
If, in Vue 2, you were augmenting Global Properties types, then you need to update it like this:
In Vue 2:
In Vue 3:
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:
- 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! 🎉
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/