From Vue.js 2 to Vue.js 3: Migrating a Large Project — Cuez Case Study

Recently, we, at TinkerList, upgraded Cuez, our web application, to Vue.js 3. Migration from Vue.js 2 to Vue.js 3 would bring several benefits, and a long-awaited upgrade was necessary to make the platform even more reliable and robust. Our development team identified the following benefits of Vue.js 3 migration:

  • Improved performance (smaller bundle size)
  • Improved security
  • Access to the latest Vue.js packages ecosystem (VueUse composables)
  • TypeScript Support
  • Better scalability (Composition API)

Although well-prepared, the journey was challenging, marked by interesting findings, pitfalls, and invaluable lessons. With this Case Study, we would like to share our study and findings that we truly hope will be an invaluable guide for teams who want to do a migration from Vue.js 2 to Vue.js 3.

Kick-off Meeting

Per our standard procedure, the team kick-started the process with a meeting to define the scope, devise a step-by-step plan, brainstorm potential problems, and determine action items.

Because we were using Quasar v1, which doesn’t support Vue.js 3, that had to be updated first. We followed this detailed migration guide conveniently prepared by the Quasar team.

The second step was to handle all Vue.js 3 deprecation in our codebase. Once again, we leveraged an already existing official migration guide available here.

Our initial estimated timeline for the migration was several months. To succeed, it was essential to divide the effort into smaller, manageable steps, to tackle ‘must-have’ tasks alongside the optional “nice-to-have” deliverables identified during the meeting.

Preparation Phase for Migration from Vue.js 2 to Vue.js 3

The preparation phase for the migration from Vue.js 2 to Vue.js 3 involved a thorough assessment of the codebase, documentation, and ensuring compatibility with Vue.js 3 for various packages and libraries. However, many packages posed challenges, requiring code refactorization and, in some cases, complete rewrites:

  • @quasar/app – the main framework package – required complete migration to v2.
  • @tiptap/vue-2 – the headless editor framework package – requires replacement with Vue.js 3-compatible package.
  • vee-validate – the main forms validation package – required replacement with the new version.
  • Vue Router – package handling app routing system – required update to Vue.js 3-compatible version.
  • Vuex – package handling global app state management – required update to Vue.js 3-compatible version.
  • Vue-i18n – plugin handling internationalization – required update to Vue.js 3-compatible version.
  • Draggable – library handling draggability feature – no upgrade available, required package replacement with vue3-draggable-resizable
  • vue-advanced-cropper – image cropper component – required update to Vue.js 3-compatible version.

Only a few packages were straightforward to upgrade – bumping package version in package.json, and all the features work with full backward compatibility. On the other end, most of the packages introduced breaking changes with their new versions. In our case, migrating to Vee-validate v4.x, vue-i18n-next, and Vue Router v4 caused the most issues and proved to be particularly demanding and time-consuming.

Feature Branch

As the next step, we opted for a feature branch instead of a code freeze, since this was a months-long process and we had to continue with our business as usual during that period. Keeping it as much up-to-date with the main branch as possible, we spent the first weeks handling deprecations. This proved to be a challenging and very exhausting process, revolving mostly around iterating on a black screen and console window with hundreds of JS errors and warnings.

A couple of weeks later, the transition to rendering the login page highlighted numerous issues. Compared to the currently stable and live version, our migration feature branch still showed many issues, including CSS inconsistencies.

Handling Deprecations and Other Issues

We started with handling general Quasar deprecations. Most of the encountered issues were already described in the official migration guide. In our case, we had to bump @quasar/app package versions in package.json file, adjust the Quasar configuration file (quasar.conf.js), and apply several changes in common components like QDialog/ QMenu/ QTooltip/ QBtn/ QItem, etc.

Global API changes

Vue.js 3 introduced a substantial number of breaking changes. It took us a few months to adapt the codebase to the required changes. For example, Vue.js 3 Global API changes require rewriting most of the plugin’s initialization:

Vue.js 2
Vue.js 3

Vue.js Component Template Directives

Changes introduced in Vue.js template directives forced us to rewrite many parts of our web app. Most common changes:

  • New usage of v-model
  • New usage of key on <template v-for>
  • v-if and v-for precedence changes
  • Removed v-on:event.native modifier
Vue.js 2
Migration from Vue.js 2 to Vue.js 3

Lifecycle Hooks Changes

Other breaking changes were related to renamed lifecycle hooks (for example, destroyed has been renamed to unmounted) and removing global functions set and delete which were heavily used in our Vuex store files.

CSS Deprecations

One of the CSS-related deprecations introduced in Vue.js 3 was a change in the usage of /deep/ selector. We had to replace it with a new ::v-deep() syntax. Sadly it wasn’t a simple “find and replace” change as starting in Vue.js 3 v-deep now requires a CSS selector as an argument, which must be passed depending on the context where it was being used.

Navigating Vue.js 3 Package Incompatibilities

For some of the packages, no Vue.js 3 versions were available — our team had to find alternatives. A good example would be Draggable package. We had to replace it with vue3-draggable-resizable. This seemingly easy replacement ended up causing many issues in our UI. By changing components, most of the time, we were also changing the HTML markup in our web app. This usually causes CSS issues as selectors no longer work properly. Similar issues affected our E2E tests — changing components usually requires E2E test selectors to be updated.

Vue-I18n Package Upgrade

The vue-i18n package upgrade process unexpectedly caused many issues. Version v9 introduced several breaking changes. Additionally, we had to resolve numerous undocumented edge cases causing our web app to crash, especially regarding message formatting. According to the vue-i18n team:

“Since Vue I18n v9 and later, message format syntax is now handled by the message format compiler if you use these special characters as part of a message, the message will occur error at compile. If you want to use these special characters, you have to use literal interpolation.”

In our case, we had for example to replace all occurrences of @ symbol in messages:

Vue Router Upgrade

Upgrading Vue Router to v4 initially caused infinite redirect loops in our web app. We were able to fix all issues by applying suggestions featured in the official breaking changes guide. One of the biggest changes was the removal of * (star or catch-all) routes. We had to replace routes using (*/ /*) with routes defined using a parameter with a custom regex:

Vue.js 2 to Vue.js 3

VeeValidate Package Upgrade

VeeValidate was the most problematic package in our case — we had to upgrade to v4.x. Unfortunately, Vue.js 3 compatible version of this library is not backward compatible with VeeValidate 3.x. That means we had to rewrite every single form in our web app. Besides new components, we also had to introduce a new validation scheme.

ESLint Rules

In the end, we had to adjust some linter rules. In our project, we are using ESLint vue/vue3-recommended preset. A good example would be v-html directive on components which is not allowed in the preset by default. For such cases, we had to disable some rules in our codebase:

Migration from Vue.js 2 to Vue.js 3

Fixing E2E Tests

Fixing E2E tests was another time- and effort-consuming task. To make this process efficient, we decided to involve more development team members at an early stage. We prepared a long list of failing tests and split the work among the team members. At this point, we effectively enforced a merge freeze in all other feature development streams, in order to avoid more tests breaking due to newly delivered functionality for example. This led to fruitful results — we fixed over a hundred failing E2E tests.

Sandbox deployment and manual testing

After handling failing E2E tests, we were ready to deploy the Vue.js 3 migration branch to our testing environment. The process was straightforward — we had to update several deployment/CI/CD scripts, bump the node.js version and rebuild Docker images used for the CI/CD pipelines.

As a next step, we’ve asked our manual testers for help. We discovered a dozen more issues. Some of the issues were very critical (categorized as blockers). It’s worth emphasizing the importance of manual verification as not everything can be covered with E2E tests.

Key Takeaways & Tips for Migration from Vue.js 2 to Vue.js 3

  • Keep your migration feature branch always up-to-date with the master branch. We learned that resolving conflicts is sometimes very difficult and time-consuming if you are not rebasing often.
  • Introduce code freeze at the last stage of migration, otherwise, you will a significant amount of time trying to keep everything in sync, resolving conflicts, and applying necessary Vue.js 3-specific fixes.
  • Don’t rush with the migration process. The initial preparation step is crucial in identifying potential roadblocks and problematic libraries/packages.
  • Don’t rely solely on automated tests — in our case, manual verification was crucial to spot some tricky hidden UI issues.
  • You don’t have to migrate every single feature at once. Converting components to <script setup> , extracting composables, and introducing TypeSctipt are optional and can be implemented later.

Rundown and Automator for Live Broadcasting


©2023 CUEZ. All Rights Reserved.