Migrating Vue 2 to Vue 3

I recently converted a second app from Vue 2 to Vue 3, added TypeScript, and swapped from options api to composition api. Here are my notes from the experience.

My 3 main target areas for this migration were:

  1. Vue 2 to Vue 3
  2. JavaScript to TypeScript
  3. Options API to Composition API

Almost every resource I used was from the official docs for Vue, the Vue CLI, the Vue Router, and Vuex. Some of these docs have sections in migrating, adding (like vue add typescript), or upgrading.

All of these have docs. Some have migration guides, some do not. Some of the migration guides tell you the differences, but don't do it from an existing app.

The steps I followed may not be the most optimal path. I would be shocked if they were. Your mileage may vary (YMMV). After digging through the docs and the Vue CLI API (to check out the add/migration commands) this is where I landed. Hopefully by sharing the steps I went through they will be helpful to some of y'all.

If you want to host your Vue app in the cloud, I recommend trying Azure Static Web Apps.

Why

If you don't care why I went down this road, you can scroll past this "Why" section.

I find it valuable to understand why I should consider doing something before I invest time into doing it. Thus, I'll explain a bit of my reasons why I chose to migrate for each of these three target areas.

Vue 3

The migration to Vue 3 is to keep up to date with Vue. I find it important stay current with major version changes of web frameworks sooner rather than later. Often the tooling supports older versions for a while, but as the ecosystem evolves, the latest versions get the love and as with most software, the older versions start to lag.

Also, keeping up with the latest version offers a lot value in the core framework improvements. One feature I won't miss is mixins, which I rarely used. I find the hooks concept or simply importing other modules of code to be much easier to follow, reuse, and maintain.

JavaScript to TypeScript

One of the most exciting to me is that Vue 3 is written with TypeScript and in my experience this helps the stability of the platform and provides much better development and tooling help.

I've converted a lot of apps to TypeScript over the years. I've also started a lot of apps with TypeScript from scratch. Adding types to the development flow almost always reveals bugs in my code that were previously undetected in my JavaScript code. I do not follow the school of "type the heck out of everything though". This is a road that, IMO in a web app, can lead to hours of time with little to no reward. I do add types. I do avoid any. But there are times to weigh the value.

Composition API

There is nothing wrong with the Options API. In fact, it is arguably one of the biggest reasons why Vue has been so approachable and easy to learn. That all said, I do find that spreading code out across my components does make it harder to read my own code vs keeping similar logic together. For this reason, I want to give the Composition API a chance. It took a lot of work to get the code converted to the Composition API.

Migration

Here is where I started. The notes below are indeed notes. They are not every single step I took. I will explain how I thought through these steps and which ones worked well for me and which ones I struggled through.

The first step for me was to get the migration kicked off on a new git branch. So I started by aking a new branch so I could track the changes.

1. Migrate from Vue 2 to Vue 3

The Vue CLI has a command to upgrade to Vue 3.

vue add vue-next

Running this command modified these files:

File Change
eslintrc.js for some Vue 3 eslint settings
package.json this upgraded Vue to 3.0.0 beta.1 and Vuex to 4.0.0-alpha.1. It made me wonder why the alpha and betas, of course. I had to manually add @vue/compiler-sfc , not sure why. But when I tried building the app, it complained about this being missing. How did I know this? I generated a new Vue 3 app and saw it there. Maybe I missed this in my existing app.
_src/main.js The createApp API replaces the new Vue({ ... }) API
_src/store/index.js The Vuex.createStore API replaced the Vue.use(Vuex) API

2. Adding TypeScript

The Vue CLI has a command to upgrade to TypeScript.

vue add typescript

Running this command modified these files:

File Change
eslintrc.js SSome extends settings changed. But instead of adding the new ones, I ended up with 2 extends arrays. So I had to manually fix this.
package.json Several typescript packages were added

| app.vue | This entire component was overwritten. I ended up with a reference to a HelloWorld component (which also was added). My app obviously doesn't need that so this was code to remove. It also added some CSS and template code that I had to remove. I had to manually revert the changes, and apply the ones that were needed for TypeScript. The key here was to revert the changes with git and apply the <script lang="ts"> and swap from export default { ... } to export default defineComponent ({ ... }) |
| *.ts files | Many javascript files were renamed to typescript files |
| shims-vue.d.ts | This typings file was added to support some Vue conventions |
| tsconfig.json | The typescript config file |

This is the second project I migrated from Vue 2 to Vue 3 and added TypeScript. I thought that this process migrated all components to TypeScript. Maybe I was wrong, because I had to manually upgrade every component this time, which leads me to the next step.

3. Fixing TypeScript

I went through every component file and applied the <script lang="ts"> and swapped from export default { ... } to export default defineComponent ({ ... }). This took a while.

4. Vue Router

The Vue CLI has a command to upgrade the Router.

vue add router

Running this command modified these files:

File Change
package.json A few router packages were added and modified. The vue router bumped up to 4.0.0-0
_src/main.ts The createApp API extended to include use(router)
_src/router.ts The createRouter API replaced the old Vue.use(Router) API. It also added the createWebHistory API which replaces the mode: history technique

When I built the app and served it, I found an error about the catch-all route in the browser console error messages. So I checked out the Vue Router docs and it said I needed to manually refactor the "catch-all" route in router.ts.

This went from this code:

{ path: '*', component: PageNotFound },

... to this code ...

{ path: '/:pathMatch(.*)*', name: 'not-found', component: PageNotFound },

This was in the Router docs, which was helpful.

5. Vuex

The Vue CLI has a command to upgrade Vuex.

vue add vuex

Running this command modified these files:

File Change
package.json A few Vuex packages were added and modified. The vuex version bumped up to 4.0.0-0

Nothing else changed, which I found surprising. I assumed the store logic would upgrade, but it did not. This lead me to manually modify the store file.

File Change
src/store/index.ts I swapped to the new crateStore API. Similar to the other API changes for Vue and the Vue Router

6. Pausing for TypeScript

Then I made a ton of TypeScript modifications. I went through all of my Vuex code and added types. This took some time, but it was worth it (and expected). This is my code and only I knew the types ... and in some cases I had to create the types in the form of types, classes, and interfaces (again, for my own code).

One key aspect here is that I customized several ESLint settings. Here is what I added to eslintrc.js. When running npm run lint the Vue compiler spits out the eslint errors and warnings.

Here is what I added.

'max-classes-per-file': 'off',
'no-useless-constructor': 'off',
'no-empty-function': 'off',
'@typescript-eslint/no-useless-constructor': 'error',
'import/prefer-default-export': 'off',
'no-use-before-define': 'off',
'@typescript-eslint/no-unused-vars': ['error'],
Setting Why
'max-classes-per-file': 'off', When I create models I often do this one per file. But in my project I had a bunch of small 5 lines of code classes and it was easier to maintain those in a single file.
'no-useless-constructor': 'off', Some of my models have empty constructors. However, they have initialization parameters that allow me to call them to create a new instance and set properties like this new Hero(1, 'John',)
'no-empty-function': 'off', Same issue as the empty constructor above.
'@typescript-eslint/no-useless-constructor': 'error', Same issue as the empty constructor above.
'import/prefer-default-export': 'off', I don't prefer default exports.
'no-use-before-define': 'off', I often put function definitions where I want them in a file. basically i use hoisting to my advantage for readability. So I turn this off in most projects.
'@typescript-eslint/no-unused-vars': ['error'], If I didn't add this, then every time I imported a type/class/interface and used it as a type, eslint complained.

Summary

Again ... the steps I followed may not be the most optimal path. I would be shocked if they were. Your mileage may vary (YMMV). After digging through the docs and the Vue CLI API (to check out the add/migration commands) this is where I landed. Hopefully by sharing the steps I went through they will be helpful to some of y'all.