Vue 2 to Vue 3 → a (nearly) painless approach

Recently, I had the pleasure of updating a Vue 2 project, that had quite a "vue" years under its belt. This guide helps you doing exactly that.

Vue 2 to Vue 3 → a (nearly) painless approach

Recently, I had the pleasure of updating a Vue 2 project, that had quite a "vue" years under its belt. There exist many guides out there, on how to do that. To be honest, most of them are bullshit, and it seems as if every guide out there just copied the official docs for migrating - without any thought.

I had to update a project, that used Vue 2.6.11, Vuetify 2.2.6 and Typescript 3.5 as well as many, many other packages. While updating I had to constantly adapt to new Typescript rules, changed Vue internal APIs as well as changed Vuetify components.

To make things easier and (almost) painless for you, I decided to share my experiences, mistakes I have made and how to avoid them with you! So let's get started.

an image depicting that programming with an old, not updated tech stack can be cumbersome
Programming with an old, not updated tech stack can be cumbersome

Why even upgrade?

Well, Vue 2 has officially reached EOL on December 31st, 2023. This means no more new features, updates or bug fixes. I therefore strongly advise you to migrate any project you would like to maintain and develop further.

Existing docs

This guide is not the only guide that exists on how to migrate. The thing is, when I needed a guide on how to do it, it couldn't find any that would fit my needs. Most of the guides that exist try to be helpful, but they aren't.

I have read things like:

  1. Update Vue 2.x to Vue 3.x in the package.json file, remove all Vue 2.x specific packages (template compiler, CLI plugins etc.)
  2. Install all new packages from the package.json file
  3. Fix the errors
  4. Fix the warnings
  5. You are ready to go!

When I read this "helpful" tips the first time, I was annoyed and thought: "Well, maybe there are other guides helping me out with upgrading Vue and Vuetify", so I opened another guide, and yet another guide etc. After reading five guides that provided me with nothing new, I gave up and decided to do it my way.

a programmer being happy, that his updating approach works as intended

My approach to updating Vue 2.6 to Vue 3.x (including Vuetify)

There are a few things to consider, before starting to update your app. Things that I thought about before updating included the following:

  1. How many packages do I have? Are there newer versions available that are compatible with Vue 3 using the options API
  2. Do I need all packages that are currently installed
  3. Do I just want to update to the latest releases without changing the API style of Vue, or do I want to adapt to the Composition API

After answering those questions, I had an understanding of which components need to be completely adopted and which would just need some refurbishment.

And then, the horrors of updating a year's old product to the latest technology began.

My first approach (Spoiler: did not work)

I followed the exact guidelines, that are outlined in almost every guide:

  1. remove Vue2 and vue-template-compiler from package.json
  2. run npm install vue, @vue/compat, @vue/compiler-sfc
  3. update vue-router and vuex to the latest version
  4. extend the Webpack config in vue.config.ts so that the @vue/compat compiler is being used as alias for Vue
  5. change the main.js (in my case main.ts) file to use the new syntax to initialize the Vue application
  6. fix the errors
  7. fix the warnings

Even after following those guides step for step, I couldn't get the application to run. At some point I realized that those guides were made for small projects, almost no external dependencies, no Typescript, no Vuetify etc. But that was not really what I was looking for...

After trying several times, it was time for me to try something new.

a programmer looking at many screens

My second approach

After reading through this official migration guide, I had an idea! It might be a brutal and direct approach to updating, but what could go wrong? The other approaches don't really work anyway for me, so I had nothing to lose.

First, I followed the migration guide from Webpack to Vite. Of course, I adopted it to suit my needs for Vue 3, Vuetify and Typescript 5. Which basically means removing everything that has @vue/cli before its name and deleting all babel packages. After updating to Vite (in theory, I couldn't run the project at this point as everything else was deprecated basically). I could begin updating my dependencies.

To update my packages, I used an extremely helpful tool called NPM Check Updates. After installing the package globally, you can just write ncu -u in the project directory, and it automatically updates all your dependencies to the latest versions. In my case, there were quite some major version jumps that needed to be installed.

To make things easier for me, I decided to install typescript-eslint, the package provided me with very good linting for Typescript 5 and helped me fix compile errors even before they occurred. Furthermore, I decided to not change all option APIs to composition APIs, because the budget for the update was limited, and I didn't want to lose too much time.

After preparing my package.json I could begin looking for problems. Oh boy, there were many of them!

Detecting the obvious issues

Obviously, as we now use Vue 3, we must change the way it is initialized. Previously this would be the recommended way:

new Vue({}).$mount('#app');

After updating to Vue 3 this now looks like this:

const app = createApp(App);
app.mount('#app');

The good thing when using the new syntax for the plugins is, that Vue directly tells you how fucked up your codebase is, and you can really just fix plugin after plugin.

Fixing the plugins

Fixing the plugins was compared to the rest that followed relatively easy and straightforward. I decided to fix Vuetify first. After doing that, I went on fixing the internationalization. By using the package vue-i18n, I was able to change as little code as possible, as this package still supports the old $t() syntax.

vue-i18n
import { createI18n } from 'vue-i18n'

import { en } from '@/locales/en';
import { de } from '@/locales/de';
import { it } from '@/locales/it';

const messages = {
  en,
  de,
  it
}

const instance = createI18n({
  locale: process.env.VUE_APP_I18N_LOCALE || 'de',
  fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'de',
  messages,
  globalInjection: true,
  allowComposition: true,
})

export function setLocale(lang: string) {
  if (lang !== 'de' && lang !== 'en' && lang !== 'it') return;
  instance.global.locale.value = lang;
}

export function getLocale() {
  return instance.global.locale;
}

export default instance;

After fixing i18n, I decided to implement the newer this.t syntax, just because it looked better and no need to configure Typescript. Basically in every component that used internationalization I just inserted:

setup() {
    const { t } = useI18n()
    return { t }
}

By following this syntax, I was able to just use STRG+F for search in each component file and replace each occurrence of $t( with t(. Et voilà: Internalization for your application is fixed.

vuex

After fixing Vuetify and i18n, I began to correctly initialize Vuex. This was as easy as could be!

The old (broken) way:

onst store = new Vuex.Store({
  modules: {
    authStore,
    userStore,
    companyStore,
    vehicleStore,
    vehicleManufacturerStore,
    companyVehicleStore,
    reportStore,
    certificationAuthorityStore,
    certificateStore,
    certificateTemplateStore,
  },
})

export default store

The new (working) way:

const store = createStore({
  modules: {
    authStore,
    userStore,
    companyStore,
    vehicleStore,
    vehicleManufacturerStore,
    companyVehicleStore,
    reportStore,
    certificationAuthorityStore,
    certificateStore,
    certificateTemplateStore,
  },
})

export default store

Done. Vuex stores are now working again.

vue-router

After moving the router file to its own directory "router" and renaming it to "index.ts", updating was quite easy:

Change this:

const router = new Router({
    mode: 'history',
    routes: [{}],
    base: 'BASE_URL'
})

to this:

const router = createRouter({
    history: createWebHistory(),
    routes: [{}]
});

That's it, so routing works as well.

By now, we have fixed several core dependencies, that have been the backbone of the application I need to upgrade:

  1. Vuetify
  2. Internationalization via vue-i18n
  3. Stores via Vuex
  4. Routing via vue-router

To continue this journey as painlessly as it has been so far, I decided to update the toast dependency that my project used to a package that supports the old syntax, so that I wouldn't have to change every single instantiation of toasts.

vue-toast-notification

I decided to use the package vue-toast-notification. It worked out of the box, nothing to configure whatsoever, I didn't even have to change any function calls and suddenly had working toasts again.

Before I finally decided to use vue-toast-notification, I struggled to find a way to make the package vuetify-toast-snackbar work again, which was used in the Vue 2 version of the project, but this project hasn't been updated for five years, so it was time to move on. As the new package vue-toast-notification provided me with the exact same functionalities and even extended the existent ones, it was the perfect fit for my needs.

To use dialog confirms (as they have been previously implemented and really liked by our customer) I decided to follow the same approach as with toasts and installed the package vuetify-use-dialog as a replacement for vuetify-confirm. Same thing here, nothing to change, worked just fine as before.

The big advantage of both packages (vuetify-use-dialog and vuetify-toast-notification) was, that altough I had nothing to change in the code base (except the instantiation in the 'main.ts' file), they worked better than the old ones, looked better and had more customization options.

vee-validate

I completely threw out vee-validate because I didn't see any added value for my use cases that Vuetify didn't provide out of the box. I really only had to validate simple things, such as unique emails, required fields etc. Some API calls, but mostly just standard regex, and Vuetify can validate pretty well (even asynchronous validation) without having to implement and install a new package like vee-validate. So I decided it would be the easiest approach to just use the already defined rules and use them with Vuetify instead of vee-validate. Examples of the rules I used that worked just fine:

import i18n from '@/i18n'
import { regexEUNumber, regexNumbersAndSpecialChars } from './helpers'
import userService from './services/userService'

const { t } = i18n.global

export const requiredRule = (value: any) => {
  // CODE
}

export const decimalRule = (value: any) => {
  // CODE
}

export const checkUserNameRule = async (value: any) => {
  // CODE
}

export const checkEmailRule = async (value: any, oldValue: any) => {
  // CODE
}

You see that even asynchronous calls can be made without any problem. To use those rules in my Vue components, I needed to change the following things:

  1. Delete all <validation-observer> tags, as they were deprecated by vee-validate already, and I wouldn't even use this package anymore
  2. Replace the observer with the following code snipet for each input:
<v-text-field
      v-model="certificationAuthority.email"
      color="secondary"
      :label="t('email') + '*'"
      prepend-icon="mdi-email"
      validate-on-blur
      variant="underlined"
      :rules="[email]"
/>

and the rules were imported in the setup:

setup() {
    const { t } = useI18n()
    const required = (value: any) => requiredRule(value)
    const email = (value: any) => emailRule(value)
    return { t, required, email }
}
  1. remove all :error-message properties from the inputs, as they are not needed anymore because Vuetify handles everything just fine
  2. Wrap each input in a v-form with a v-model so that you can check whether the form is valid or not:
<v-form v-model="validCertificationAuthorityContact" validate-on-blur>
    <v-text-field
      v-model="certificationAuthority.email"
      color="secondary"
      :label="t('email') + '*'"
      prepend-icon="mdi-email"
      validate-on-blur
      variant="underlined"
      :rules="[email]"
    />
</v-form>
  1. in the saving function, just check if the variable is truthy. Et voilà: Working validation without any third party library.

Detecting the not so obvious issues

Excited to see the project with the newly updated dependencies, I was quite disappointed, when I saw that no data-table showed anything. But the issue was found quickly:

Vuetify data tables changed the way they are handling their data, previously the text and the value of each item was accessed via:

headers(): any {
    return [
        {
            text: 'TEXT',
            value: 'KEY'
        },
    ]
}

this changed to:

headers(): any {
    return [
        {
            title: 'TEXT',
            key: 'KEY'
        },
    ]
}

And suddenly, data was in my tables.

Other issues, that were more or less obvious, were for example deprecated properties on <v-btn> components, or deprecated properties and tags in general. But by now your application should be running Vite, so you can just click through and look for things that aren't quite right. In my case, it was mainly formatting and layouts that were not created correctly.

a happy programmer, because everything just worked out

I really hope that this guide helped you to migrate Vue 2 to Vue 3! If you have any questions or feedback, feel free to leave a comment. Thank you for reading!

For more blogs like this, feel free to sign up: