7 Useful Tips for Migrating from Vue 2 to Vue 3

Migrating from Vue 2 to Vue 3 can be a complex task, but with the right knowledge and approach, it doesn’t have to be overwhelming. In this guide, we will explore the top tips and best practices for a successful migration from Vue 2 to Vue 3. Whether you are a seasoned Vue developer or new to the framework, this article will provide you with the essential information you need to smoothly transition your Vue 2 project to Vue 3.

Why Upgrade to Vue 3?

Before we dive into the nitty-gritty of the migration process, let’s talk about why you should upgrade to Vue 3. Vue 3 comes with a host of new features and improvements that make it faster, smaller, and more efficient than Vue 2. With the new Composition API, reactivity system, and better TypeScript support, Vue 3 is a game-changer for web development.

Useful Tips

  1. main.js
  2. Options API to Composition API
  3. Vue Router
  4. Vue.filter Deprecated
  5. beforeDestroy Renamed
  6. Dependencies
  7. Convert usage of .native and $listeners

main.js

In Vue 3, the createApp function is used to create the application instance, and the mount method is used to mount the app to the DOM element with the id of app. This simplifies the code compared to Vue 2, where we had to create a new Vue instance and use the $mount method to mount the app.

// === Vue 2 ===
import Vue from 'vue'
import App from './App.vue'

new Vue({
  render: h => h(App),
}).$mount('#app')

// === Vue 3 ===
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')
    

Options API to Composition API

In Vue 3, you have two options for defining the composition logic of a component: using the setup() function or the <script setup> syntax.

<!-- === Vue 2 === -->
<template>
  <div>
    <p>{{ message }}</p>
    <button @click="updateMessage">Change Message</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: 'Hello, Vue!'
    };
  },
  methods: {
    updateMessage() {
      this.message = 'Welcome to Vue 3!';
    }
  }
};
</script>

<!-- === Vue 3 using <script setup> === -->
<template>
  <div>
    <p>{{ message }}</p>
    <button @click="updateMessage">Change Message</button>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const message = ref('Hello, Vue!');

const updateMessage = () => {
  message.value = 'Welcome to Vue 3!';
}
</script>

<!-- === Vue 3 using setup() function === -->
<template>
  <div>
    <p>{{ message }}</p>
    <button @click="updateMessage">Change Message</button>
  </div>
</template>

<script>
import { ref } from 'vue';

export default {
  setup() {
    const message = ref('Hello, Vue!');

    const updateMessage = () => {
      message.value = 'Welcome to Vue 3!';
    }

    return {
      message,
      updateMessage,
    };
  }
};
</script>
    

Vue Router

// === Vue 2 ===
import Vue from 'vue';
import VueRouter from 'vue-router';
import Home from './components/Home.vue';
import About from './components/About.vue';

Vue.use(VueRouter);

const router = new VueRouter({
  routes: [
    { path: '/', component: Home },
    { path: '/about', component: About }
  ]
});

// === Vue 3 ===
import { createRouter, createWebHistory } from 'vue-router';
import Home from './components/Home.vue';
import About from './components/About.vue';

const router = createRouter({
  // Remove hash (#) from url, as opposed to createWebHashHistory which uses hash
  history: createWebHistory(),
  routes: [
    { path: '/', component: Home },
    { path: '/about', component: About }
  ]
});
    

Vue.filter Deprecated

In Vue 3, instead of directly registering filters on the Vue constructor, we create a global filter by adding it to app.config.globalProperties.$filters. This makes the filter available globally in the application. To use the filter in a template, we prepend $filters to the filter name.

// === Vue 2 ===
// Define a filter
Vue.filter('capitalize', function(value) {
  if (!value) return ''
  value = value.toString()
  return value.charAt(0).toUpperCase() + value.slice(1)
})

// Use the filter in a template
{{ message | capitalize }}

// === Vue 3 ===
import { createApp } from 'vue'

// Create a global filter
const app = createApp({})
app.config.globalProperties.$filters = {
  capitalize(value) {
    if (!value) return ''
    value = value.toString()
    return value.charAt(0).toUpperCase() + value.slice(1)
  }
}

// Use the filter in a template
{{ $filters.capitalize(message) }}
    

beforeDestroy Renamed

In Vue 3, the lifecycle hook beforeDestroy has been renamed to beforeUnmount. You can import the onBeforeUnmount function from the vue package and use it inside the setup function to perform any necessary clean-up or actions before the component is unmounted.

// === Vue 2 ===
export default {
  // ...
  beforeDestroy() {
    // Do something before the component is destroyed
  },
  // ...
}

// === Vue 3 ===
import { onBeforeUnmount } from 'vue';

export default {
  // ...
  setup() {
    onBeforeUnmount(() => {
      // Do something before the component is unmounted
    });
  },
  // ...
}
    

Dependencies

  • Update all your dependencies to their latest versions, ensuring compatibility with Vue 3.
  • Consider using the @vue/compat library to smooth the transition and maintain temporary compatibility with older dependencies.

    # Install via npm:
    npm install @vue/compat --save-dev
    
    # Install via Yarn:
    yarn add @vue/compat -D
    
    # Install via pnpm:
    pnpm add @vue/compat -D
            

Convert usage of .native and $listeners

  • In parent component, remove .native, normally attached to @click
  • In child component, make the following adjustments:
  • // Important: The code below is added to the child component `.vue` file, not `vite.config.js`
    export default {
      // `compatConfig` setting should be removed when `@vue/compat` is uninstalled
      compatConfig: {
        MODE: 3,
      },
      // `inheritAttrs: false` will prevent root element from inheriting $attrs
      inheritAttrs: false,
      ...
    }
          
  • Add v-bind="$attrs" to the element that emits event that should be passed to the parent. This should replace v-on="$listeners" if the attribute exists.
  • Ensure event to be emitted is not in the emits: [] array (by default this is not used in Vue2)
  • Note: Assigning v-bind="$attrs" to one element will resolves warning: [Vue warn]: Extraneous non-props attributes (data-version) were passed to component but could not be automatically inherited because component renders fragment or text root nodes.
  • Without @vue/compat, only need the following (i.e. compatConfig not needed):
  • // === Vue 2 ===
    // ParentComponent.vue
    <template>
      <ChildComponent @click.native="doSomething">
    </template>
    
    // ChildComponent.vue
    <template>
      <button v-on="$listeners">Click here to do something</button>
    </template>
    
    // === Vue 3 ===
    // ParentComponent.vue
    <template>
      <ChildComponent @click="doSomething">
    </template>
    
    // ChildComponent.vue
    <template>
      <button v-bind="$attrs">Click here to do something</button>
    </template>
          

Bonus Tips

  • Convert slot="..." to v-slot:... or shorthand #...
  • Convert @vue/composition-api to vue, e.g. import { reactive } from '@vue/composition-api' to import { reactive } from 'vue'
  • Change environment variable access, e.g. From process.env.BASE_URL to import.meta.env.BASE_URL

Conclusion

Migrating from Vue 2 to Vue 3 is an exciting opportunity to leverage new features, improve code organization, and enhance the performance of your Vue applications. By following these top tips and best practices, you can navigate the migration process successfully and take full advantage of the power of Vue 3. Remember to plan your migration strategy, understand the changes, and address any breaking changes to ensure a smooth and efficient transition. Happy migrating!

Share: