From Vue 2 to 3: A long journey
Tags: vue, typescript, javascript, webdev
Published: 2023-08-17 14:19:49

## 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](https://blog.vuejs.org/posts/vue-3-as-the-new-default)

## 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](https://github.com/vuejs/vue-class-component) with a combination of [vue-property-decorator](https://github.com/kaorun343/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

```js
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](https://marketplace.visualstudio.com/items?itemName=vue.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](https://www.npmjs.com/package/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.

```js
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:

1. We used an internal UI library which depended on components that weren't compatible or were unstable with Vue 3 at the time
  1. [vee-validate](https://vee-validate.logaretm.com/v3/)
  1. [v-tooltip](https://www.npmjs.com/package/v-tooltip)
  1. [vue-showdown](https://vue-showdown.js.org/)
  1. [vue-multiselect](https://vue-multiselect.js.org/)
  1. [vue-grid-layout](https://jbaysolutions.github.io/vue-grid-layout/)
1. 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.
1. 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).

![I thought about that initially, but aside from how hard is it to find a new name, I think it is better to build on the existing popularity of vee-validate especially not everyone who used vee-validate will switch to the new package and I would be gettings tons of requests for Vue 3 support.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ps1lbe6n086plryn1f1g.png)

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](https://v3-migration.vuejs.org/breaking-changes/slots-unification.html) 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](https://github.com/logaretm/vee-validate/blob/v3/src/components/Provider.ts#L215) of the code.

![Code example of where slots don't work](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gnw2i9bfgvugbzs0akyq.png)

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](https://github.com/vuejs/vue/blob/main/CHANGELOG.md#270-2022-07-01) 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](https://github.com/vuejs/vue-class-component/pull/614) to get inter-component type inference to work.

[@matrunchyk](https://github.com/matrunchyk) had done a fantastic job [creating a codemod](https://github.com/vuejs/vue-codemod/pull/32) 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](https://github.com/NikhilVerma/vue-codemod/blob/main/transformations/vue2-class-component-to-native-typescript.ts). 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 |
| 23nd 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](https://flow.cldcvr.com/) library which used [Lit](https://lit.dev/). 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:

1. Migrate all old form and vee-validate instances with the new Lit ones
1. Migrate remaining UI components in the library to support Vue 3
1. 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)

![PR overview of the Vue migration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x7axiuv9j2cmxkh64uv3.png)

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](https://v3-migration.vuejs.org/breaking-changes/attribute-coercion.html) 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.