How we migrated to Vue 3

Sahin Deniz
Insider Engineering
8 min readDec 5, 2022

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

Credit goes to dear Ozlem Guder

This article is not a guide on how to migrate Vue 3.
It’s a story of how we made it, alive 🤓

Just like any other technology company out there, here at Insider, we also have faced this ultimate dilemma.
Upgrading the framework of one of our Vue front-end apps 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 consider the possible outcomes of the upgrading process.

For the business part, we made sure that we can do this technical 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 about why migration is necessary for your product.

If you are also looking for benchmark results and comparisons between Vue 2 and Vue 3, here are some articles that you can benefit from:

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, what breaking changes we will 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 technical debt items.

So, as suggested in this article, I have upgraded the packages like the following:

   "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 a new way :deep(). I have changed all usages.

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

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

And then there was the deprecated app entry, and the guide suggests that the new global mounting API needs to be used.
Before:

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

Vue.directive('tooltip', tooltip);
Vue.use(VueI18n);

After:

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

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

https://gist.github.com/seahindeniz/f25758b4ed077e5997422f1d2a2ec0d8

Changing Vue-CLI with Vite

While investigating Vue 3 for further breaking changes, I’ve come across 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 application, I have seen some subscription-related issues.
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 application calls an action declared in a Pinia store, Pinia calls subscribed listeners via the $onAction method, before mutating the state. The callback function will be called with an object parameter called context and it will have a property/method called after which gets 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 dealbreaker for us, but once we understand of when it actually gets called, it was quite a lifesaver.

The @ckpack/vue-color

We continued to modify our application, 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 application again, I’ve seen that we had issues with some browserify-related polyfills.

We have used the following solution.

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 extensions.

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

Me, and some of my colleagues, are using VSCode ✨. So we’ve switched from Vetur to 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 cycle.

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

Before:

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

After:

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.

--

--