How we migrated to Vue 3
Tags: vue, vue3, migration, pinia
Published: 2022-10-09 17:09:55

In this article, I'll try to share with you an overview of the Vue 3 migration journey.

><sub>This article is not a guide on how to migrate Vue 3.
It's a story of how we made it, alive 🤓</sub>

---

Just like any other technology company out there, here at Insider, we also have faced this ultimate dilemma.
Upgrading our front-end framework to the new version of Vue.

This journey started with a discussion of course. On a sunny day, I don't remember when, we talked about Vue 3, and the point that it's becoming the new default, and we are still using the old version.

We wanted to switch, but we needed to make sure that this "new default" is gaining traction and support from the community, just like everyone.

We hesitated to upgrade long before because some third-party libraries out there that we may need at some point may not support Vue 3!
Just because of this reason, we've waited till Vue 3 is stable and adapted well.

Time has passed and Vue 2 has been marked as in maintenance mode. So we have started to make plans, and experiments on our products, research the migration process, and considered the possible outcomes of the upgrading process.

For the business part, we made sure that we can do this tech debt within our sprints, without affecting anything.

And we started!

But just to let you know, I'm not actually into the reason why this upgrade is needed.
There are plenty of resources and research out there about why migration is necessary for your product.

---

## So, here we go;
First, I have read the Vue 3 Migration Guide https://v3-migration.vuejs.org/

I have shared the crucial points with my teammates and what breaking changes we will be going to face and how we can write a new code after the upgrade.

Since we are using the Options API, changing the structure was the scariest part. However, after the research, we understand that we don't need to convert our components, and files to the new Composition API.

They both are supported and can be used at the same time.
We discussed and have made a new rule for our codebase. We will use the new Composition API when creating a new component.
Of course, using both APIs together creates another tech debt item, but as you can guess, we couldn't afford to spend efforts to convert files. This can be done in the next sprints so that we can have new tech debt items.

So, as suggested in [this article](https://v3-migration.vuejs.org/migration-build.html#upgrade-workflow), I have upgraded the packages like the following:

```diff
   "dependencies": {
     // ...
-    "vue": "2.6.11",
-    "vue-i18n": "8.26.5",
-    "vuex": "3.0.1"
+    "@vue/compat": "3.2.37",
+    "vue": "3.2.37",
+    "vue-i18n": "9.1.10",
+    "vuex": "4.0.2"
   },
   "devDependencies": {
     // ...
-    "@babel/eslint-parser": "7.16.5",
-    "@vue/cli-plugin-babel": "4.5.0",
-    "@vue/cli-service": "4.5.0",
-    "node-sass": "4.14.1",
-    "sass-loader": "8.0.0",
-    "vue-template-compiler": "2.6.11"
+    "@babel/eslint-parser": "7.18.2",
+    "@vue/cli-plugin-babel": "5.0.8",
+    "@vue/cli-service": "^5.0.8",
+    "@vue/compiler-sfc": "3.2.37",
+    "sass": "1.53.0",
+    "sass-loader": "13.0.2",
   },
```

You see, we are also expecting to upgrade Vuex too. But our next plan is to replace it with Pinia. It's the officially supported state management library by the Vue core team currently, and Vuex<4 has reached its final, and it's been in maintenance mode for a while.

So, after changing dependencies I have tried bundling the app and as the migration guide suggests, I have seen many errors about some deprecated things.

The first error that I have focused on is about the `/deep/` selectors. It has been deprecated and the Vue documentation suggests the new way `:deep()`. I have changed all usages.

Then I saw that there's an error about `util` and `assert` library polyfills required because of another third-party library we use, and the WebPack 5 upgrade made them required.

After trying to bundle again, new warnings/errors popped for the deprecated usages within our files.
Such as `v-bind`, `template slot`, `v-enter` and `v-leave` transition keys, usages with v-for and v-if together.

And then there was the deprecated app entry, and the guide suggested that the new global mounting API needs to be used.
I have changed the file like the following:

```js
const vueInstance = new Vue({
    store,
    render: h => h(App),
}).$mount(rootContainer);

Vue.directive('tooltip', tooltip);
Vue.use(VueI18n);
```
```js
const app = createApp(App);
const i18nInstance = createI18n({ locale, messages });

app.use(store);
app.directive('tooltip', tooltip);
app.use(i18nInstance);
app.mount(rootContainer);
```

After re-bundling again, there were some hooks like `beforeDestroy`, and `destroyed` are deprecated. So they needed to be changed as well.

I repeated this process until the project got clear from all warnings, and errors and make sure that our app works.

So, here I'm at the point where frustrations begin to appear.

First, it's the `vue-i18n` library. I have come across this issue  https://github.com/kazupon/vue-i18n/issues/1493 and right now, it's not yet solved.
So I decided to remove `vue-i18n` since we are not using it in an advanced way (pluralization etc.). Just a simple, key-value matching, nothing fancy.

## State Management Part

And then there's the state management library issue. Before diving into Vuex problems, we simply switched to Pinia and made integrations described in its guide.

This journey started by following this guide: https://pinia.vuejs.org/cookbook/migration-vuex.html

The conversion is quite simple and delightful for creating store files, and the process is explained quite well in that article.

Before the migration, we have the following structure on our app:
```
- store
  - common
    - index.js
    - types.js
  ...
```
We simply changed it to:
```
- stores
  - common.js
  ...
```

The files can be separated into submodules as well. But for our use cases, this folder structure was quite enough.

The part, that we no longer need to have mutations with Pinia, is the most exciting thing about this migration. I always think that there's no point in having actions and mutations.

So starting with the replacement procedure, we have created store files.

Then we search the entire codebase by finding each `vuex` and `store` usages. We pass through every single file and replace all Vuex usages with Pinia.

## The Docker
Yeah, we were using an old version of NodeJS(v14) and we also update the NodeJS version that is being declared in our Dockerfile.

## The new `emits` property
Our `.vue` files are also emitting custom events to their parents and this leads to an issue for us. I didn't want to bother changing all files manually, just for the emits, so I wrote a little node script that could handle this for us.

This small script will create the `emits` property by collecting event names across `.vue` files
{% collapsible addEmits.js %} 

```js
const fs = require('fs');
const glob = require('glob');

const directories = glob.sync('./src/**/*.vue');

class FileHandler {
    /**
     * @param {string} path
     * @param {string} content
     */
    constructor (path, content) {
        this.path = path;
        this.content = content;

        this.collectEventNames();

        if (!this.eventNames) {
            return;
        }

        this.addEmitsLine();
    }

    collectEventNames () {
        const matches = this.content.match(/(?<=\$emit\()'.*?'(?=[,)])/g);

        if (!matches?.length) {
            return;
        }

        const eventNames = [...new Set(matches)];

        eventNames.sort();

        this.eventNames = eventNames.join(', ');
    }

    addEmitsLine () {
        if (!this.eventNames) {
            return;
        }

        const newLine = `export default {\n    emits: [${this.eventNames}],`;

        this.content = this.content.replace('export default {', newLine);

        fs.writeFileSync(this.path, this.content, 'utf-8');
    }
}

const fileWithEmitsProp = [];

directories.forEach(filePath => {
    const content = fs.readFileSync(filePath, 'utf-8');

    if (!content.includes('$emit')) {
        return;
    }

    if (content.includes('emits:')) {
        fileWithEmitsProp.push(filePath);

        return;
    }

    new FileHandler(filePath, content);
});

if (fileWithEmitsProp.length) {
    console.log('File paths with `emits` props:', fileWithEmitsProp);
}
```
{% endcollapsible %}

## Changing Vue-CLI with Vite
While investigating Vue 3 for further breaking-changes, I've come across with the Vue-CLI repository. Seems like it was also marked as in maintenance mode. At first, we didn't have any plans on changing the Vue-CLI.
Since we have the opportunity to change the compiler, we touch the Vite as well.
Through the migration, I have followed an article, `How to Migrate from Vue CLI to Vite` by Daniel Kelly

https://vueschool.io/articles/vuejs-tutorials/how-to-migrate-from-vue-cli-to-vite/

The article covered the essential headlines that we need.

## Pinia's subscription flow
After building again and running our app, I have seen some subscription-related issues. 
I then noticed that Pinia was handling subscriptions differently than Vuex.
On our app, we were subscribing to the store and listening to every mutation that was used, and handling the logic accordingly.
However, by looking at the docs of Pinia, we have noticed that there's a different behavior that requires a little bit of refactoring.

When the app calls an action declared in a Pinia store, Pinia calls subscribed listeners via the `$onAction` method, before mutating the state. The callback function will get a parameter called `context` and it will have a method called `after` which will be called after the mutation has been made.
Explained here: https://pinia.vuejs.org/core-concepts/actions.html#subscribing-to-actions

Before we notice the `after` callback, this was like a deal breaker for us but once we notice the `after` callback, it was quite a lifesaver.

## The `@ckpack/vue-color`
We continued to modify our app and we also saw that we have a dependency that was not working correctly. After searching again, I noticed that it doesn't support Vue 3 so we had to replace it with another library called `@ckpack/vue-color`

## Migrating webpack resolvers

After trying to build the app again, I've seen that we had issues with some browserify related polyfills.

We have used the following solution 

```js

import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill';
import { NodeModulesPolyfillPlugin } from '@esbuild-plugins/node-modules-polyfill';
import rollupNodePolyFill from 'rollup-plugin-node-polyfills';
import { defineConfig } from 'vite';


export default defineConfig(() => ({
  resolve: {
    alias: {
      vue: 'vue/dist/vue.esm-bundler.js',
      util: 'rollup-plugin-node-polyfills/polyfills/util',
      events: 'rollup-plugin-node-polyfills/polyfills/events',
      assert: 'rollup-plugin-node-polyfills/polyfills/assert',
      buffer: 'rollup-plugin-node-polyfills/polyfills/buffer-es6',
      process: 'rollup-plugin-node-polyfills/polyfills/process-es6',
    },
  },
  build: {
    minify: !isDevelopment,
    rollupOptions: {
      plugins: [
        rollupNodePolyFill(),
      ],
    },
  },
  optimizeDeps: {
    esbuildOptions: {
      define: {
        global: 'globalThis',
      },
      plugins: [
        NodeGlobalsPolyfillPlugin({
          process: true,
          buffer: true,
        }),
        NodeModulesPolyfillPlugin(),
      ],
    },
  },
});
```

## Editor/IDE support
For the editor integration, we also need to migrate some extensions.

However, this part doesn't concern IntelliJ users since it supports both Vue 2 and Vue 3 out of the box.

But me and some of my colleagues, are using VSCode :sparkles:.
We've switched from [Vetur](https://marketplace.visualstudio.com/items?itemName=octref.vetur) to [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) extension to make sure that our editor shows the right syntax highlighting, etc. for Vue 3 files.

## Switching to Composition API

After learning that Vue 3 still supports Options API, we didn't change the whole component structure at first, all together.
We decided that we can refactor files later and we can migrate small components to Composition API through the next development.

However, we have to change each component and wrap exported objects with `defineComponent` function.

Before:
```js
export default {
  components: {
      Box,
  },
  props: {
      prop1: {
          type: Array,
          required: true,
      },
  }
};
```

After:
```js
export default defineComponent({
  components: {
      Box,
  },
  props: {
      prop1: {
          type: Array,
          required: true,
      },
  }
});
```

# Conclusion

Yes, it was quite an exhausting journey.
I still think that it is hard and maybe impossible to find every outcome while researching the migration and look out for every possible blocker at first glance without actually experimenting with it.

Was it worth it?
Definitely! You gain a lot more than before.