From Vue 2 to 3: A long journey
This is a mirror of the original post on dev.to. For discussion and comments, please visit the original post.
TL;DR
This is a long post detailing my journey of migrating a project from Vue 2 to 3. It contains the setbacks and the victories I faced and providing breakdown of the issues as well.
1. Learning Vue
I joined my current team on May 2021, the project I was working with used Vue 2.6 And Vuex 3.4 and plain Javascript.
Vue 3.0 was already out at that point yet it was not the “default” Vue version. It won’t be the case until Feb 7, 2022
2. The need for types
I was new to Vue back then and came from a React and Typescript codebase.
My first commit was to add a tsconfig.json just to have some intellisense. The pain point at that time was that Vue.extend for Vue 2 had poor support for Typescript. I wanted to fix it.
The solution came with the usage of vue-class-component with a combination of vue-property-decorator. Using a combination of the two allowed us to define components in a way that provide a reasonable amount of type safety.
The Vue component code would look something like this:
import { Vue, Component, Prop } from "vue-property-decorator";
@Component
export default class YourComponent extends Vue {
@Prop(Number) readonly propA: number | undefined;
@Prop({ default: "default value" }) readonly propB!: string;
@Prop([String, Boolean]) readonly propC: string | boolean | undefined;
}
The migration to class components was done incrementally, any new features or components written using the class syntax. Older components were refactored out whenever we needed to change them.
We also switched our VSCode plugin from Vetur to Volar which had better Typescript support.
3. Using Vuex wrong
The Vuex stores were in the “traditional” redux style. They had a file structure like so:
store/
actions.js
getters.js
index.js
mutations.js
state.js
types.js
Good luck finding the right store by name! Or landing on the right action/mutation because there was no support for intellisense.
This got solved by migrating the stores to use vuex-module-decorators which came with a much better support for Typescript. Plus you get all actions/mutations/getters etc in a single file so it’s easier to understand. This was also done incrementally.
Here is how the syntax for the decorator stores will look like:
import { Module, VuexModule, Mutation, Action } from "vuex-module-decorators";
@Module
export default class Counter extends VuexModule {
count = 0;
@Mutation
increment(delta: number) {
this.count += delta;
}
@Action({ commit: "increment" })
incr() {
return 5;
}
}
4. First setback
There was a spike done into the feasibility of migrating to Vue 3. It didn’t last long because of the following reasons:
We used an internal UI library which depended on components that weren’t compatible or were unstable with Vue 3 at the time:
We couldn’t figure out a reasonable strategy to migrate the UI library to Vue 3. Because then we would need to manage two versions of the same library.
We did not want to stop shipping our product while the migration was happening, it either had to be incremental or quick.
5. vee-validate and the $slots problem
The biggest hurdle that we faced was vee-validate. It is a form validation library built for Vue. The pain was that the library API for Vue 2 and 3 were entirely different.
You would almost call it another library, this was due to the new breaking $slots implementation in Vue 3 (more on that later).
Our UI library had used vee-validate to create a “form builder” that was used over 30 times in our application. We couldn’t find an incremental upgrade path here. Either we migrate them all or we migrate none.
6. $slots between Vue 2 and Vue 3
The official migration documentation for slots unification goes over the API differences. It omits a major point: In Vue 3 it’s not possible to get the DOM elements of the slots directly from the reference to the slot.
You would need to render your parent, then use the parent’s HTML node to get the child slot selector. Or use some kind of inject/provide workaround.
This is the main reason why the vee-validate library could not use it’s earlier, simpler API.
Here is a link to the problematic part of the code.
This wouldn’t work anymore in Vue 3 because you can’t directly extract the DOM nodes of children from the $slots property.
7. A new hope
On 1st July 2022 Vue 2.7 was released. It finally ported many features from Vue 3 back to Vue 2. This was a pivotal moment in our development because we it came with a solid Typescript support out of the box using defineComponent. This was better than vue-class-component because it needed some hacks to get inter-component type inference to work.
@matrunchyk had done a fantastic job creating a codemod to automatically migrate Vue class components to the earlier syntax. It was not perfect but workable.
I made some further modifications to it to get it to work on our project. It’s available here if you want to try it for yourself: NikhilVerma/vue-codemod. It’s still not polished enough to consider an upstream pull request but you can hack around to migrate your project with it.
We finally migrated to Vue 2.7 and defineComponent using the codemod in a very short time. Also used this to migrate most of the codebase to Typescript. Here is the timeline recorded in our JIRA ticket.
| Date | JS Files |
|---|---|
| 5th December 2021 | 244 |
| 22nd Feb 2022 | 173 |
| 23rd Feb 2022 | 138 |
| 24th Feb 2022 | 109 |
| 10th Mar 2022 | 103 |
| 7 Apr 2022 | 88 |
| 26 Apr 2022 | 54 |
| 9 May 2023 | 24 |
| 17 Aug 2023 | 22 |
At the same time, we noticed several library dependencies migrated to support Vue 3 with minimal API changes. This was exciting!
8. The solution to vee-validate
Now the only thing preventing a smooth migration was the vee-validate library. We were in luck because our UI team was working on a second version of the Flow UI library which used Lit. It also offered a replacement Form builder which could take over the vee-validate one.
Since the new library used Lit, it was agnostic of Vue 2 or 3 so incremental migration was easy.
9. Let’s do this!
The final plan was in place:
- Migrate all old form and vee-validate instances with the new Lit ones
- Migrate remaining UI components in the library to support Vue 3
- Migrate the application to Vue 3 in a single PR
The first step took around a month to complete, as the instances has to be migrated incrementally. The second step was faster, around a week to migrate because most of the other libraries had good backwards compatibility.
The third was easy too. Vue codemod helped a lot, and ESLint + Typescript caught most issues. The lifesaver was our automation suite which made sure we didn’t break any critical functionality in the application.
Here is a screenshot of the pull request which migrated our app to Vue 3. (edited out the internal bits)
After almost two years. We migrated to Vue 3. Because we prepared so well the final merge was uneventful. We had a few bugs discovered afterwards, mostly due to how boolean attributes have changed in Vue 3.
10. Aftermath
We are happily using Vue 3 since then. And I hope that future Vue upgrades will be less eventful and not need writing posts like these.
I hope this was helpful to you, I am sure that there are others who are still using Vue 2 on larger codebase and having a tough time with it. And I hope by sharing their experiences it will encourage others to start their migration journey.


