⚡ TECH BLOG
Home
Blog
Tags
About
⚡

Powered by Next.js 15 & Modern Web Tech ⚡

Back to Home

Vue 2 to Vue 3 Migration Guide: A Smooth Transition

July 15, 2021
vuemigrationjavascriptfrontend
Vue 2 to Vue 3 Migration Guide: A Smooth Transition

Vue 2 to Vue 3 Migration Guide: A Smooth Transition

Migrating from Vue 2 to Vue 3 requires understanding the breaking changes and new features. This guide will help you navigate the transition smoothly.

Breaking Changes Overview

Global API Changes

Vue 3 introduces a new global API:

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

Vue.config.productionTip = false;
Vue.use(VueRouter);
Vue.component('my-component', {});

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

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

const app = createApp(App);
app.use(router);
app.component('my-component', {});
app.mount('#app');

Multiple App Instances

// Vue 3 allows multiple independent apps
const app1 = createApp(App1);
app1.mount('#app1');

const app2 = createApp(App2);
app2.mount('#app2');

// Each app has its own configuration
app1.config.errorHandler = (err) => console.log('App 1:', err);
app2.config.errorHandler = (err) => console.log('App 2:', err);

v-model Changes

Vue 3 unifies v-model syntax:

<!-- Vue 2 -->
<CustomInput v-model="value" />

<!-- Vue 2 component -->
<input :value="value" @input="$emit('input', $event.target.value)" />

<!-- Vue 3 -->
<CustomInput v-model="value" />
<CustomInput v-model:title="title" />

<!-- Vue 3 component -->
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
<input :value="title" @input="$emit('update:title', $event.target.value)" />

Multiple v-models

<!-- Vue 3 supports multiple v-models -->
<UserForm
  v-model:firstName="firstName"
  v-model:lastName="lastName"
/>
<script>
export default {
  props: {
    firstName: String,
    lastName: String
  },
  emits: ['update:firstName', 'update:lastName']
}
</script>

<template>
  <input
    :value="firstName"
    @input="$emit('update:firstName', $event.target.value)"
  />
  <input
    :value="lastName"
    @input="$emit('update:lastName', $event.target.value)"
  />
</template>

v-if and v-for Priority

<!-- Vue 2: v-for has higher priority -->
<!-- Vue 3: v-if has higher priority -->

<!-- This won't work in Vue 3 -->
<div v-for="item in items" v-if="item.isActive">
  {{ item.name }}
</div>

<!-- Solution 1: Use computed -->
<div v-for="item in activeItems">
  {{ item.name }}
</div>

<!-- Solution 2: Use wrapper -->
<template v-for="item in items" :key="item.id">
  <div v-if="item.isActive">
    {{ item.name }}
  </div>
</template>

Lifecycle Hooks

Hook names have changed slightly:

// Vue 2            // Vue 3
beforeCreate   →   beforeCreate
created        →   created
beforeMount    →   onBeforeMount  // was beforeMount
mounted        →   onMounted
beforeUpdate   →   onBeforeUpdate
updated        →   onUpdated
beforeDestroy  →   onBeforeUnmount  // renamed!
destroyed      →   onUnmounted      // renamed!
errorCaptured  →   onErrorCaptured

Filters Removed

Vue 3 removes filters:

<!-- Vue 2 -->
<p>{{ message | capitalize }}</p>

<!-- Vue 3: Use methods or computed -->
<p>{{ capitalize(message) }}</p>
<p>{{ capitalizedMessage }}</p>

<script>
export default {
  computed: {
    capitalizedMessage() {
      return this.message.charAt(0).toUpperCase() + this.message.slice(1);
    }
  },
  methods: {
    capitalize(value) {
      return value.charAt(0).toUpperCase() + value.slice(1);
    }
  }
}
</script>

<!-- Or use global method -->
app.config.globalProperties.$filters = {
  capitalize(value) {
    return value.charAt(0).toUpperCase() + value.slice(1);
  }
};

Teleport Component

Vue 3 introduces Teleport for rendering outside component hierarchy:

<template>
  <button @click="showModal = true">Open Modal</button>
  
  <Teleport to="body">
    <div v-if="showModal" class="modal">
      <p>Modal content</p>
      <button @click="showModal = false">Close</button>
    </div>
  </Teleport>
</template>

Fragments

Vue 3 supports multiple root elements:

<!-- Vue 2: Single root required -->
<template>
  <div>
    <h1>Title</h1>
    <p>Content</p>
  </div>
</template>

<!-- Vue 3: Multiple roots allowed -->
<template>
  <h1>Title</h1>
  <p>Content</p>
</template>

emits Option

Declare emitted events explicitly:

<script>
export default {
  emits: ['update:modelValue', 'submit'],
  
  // With validation
  emits: {
    'update:modelValue': (value) => typeof value === 'string',
    'submit': (payload) => {
      return payload.email && payload.password;
    }
  }
}
</script>

Migration Build

Use the migration build for gradual migration:

// vue.config.js (Vue CLI)
module.exports = {
  chainWebpack: config => {
    config.resolve.alias.set('vue', '@vue/compat')
  }
}

// vite.config.js
export default {
  resolve: {
    alias: {
      vue: '@vue/compat'
    }
  }
}

Enable compatibility features:

app.config.compilerOptions = {
  compatConfig: {
    MODE: 2,
    FILTERS: true,
    GLOBAL_MOUNT: true
  }
}

Composition API in Options API

You can use Composition API features in Vue 2 with @vue/composition-api:

// Vue 2 with plugin
import VueCompositionAPI from '@vue/composition-api';
Vue.use(VueCompositionAPI);

// Then use setup()
export default {
  setup() {
    const count = ref(0);
    return { count };
  }
}

Store Migration (Vuex)

// Vuex 3 (Vue 2)
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export default new Vuex.Store({
  state: {},
  mutations: {},
  actions: {}
});

// Vuex 4 (Vue 3)
import { createStore } from 'vuex';

export default createStore({
  state: {},
  mutations: {},
  actions: {}
});

// In main.js
import store from './store';
app.use(store);

Router Migration (Vue Router)

// Vue Router 3 (Vue 2)
import VueRouter from 'vue-router';
Vue.use(VueRouter);

export default new VueRouter({
  routes: []
});

// Vue Router 4 (Vue 3)
import { createRouter, createWebHistory } from 'vue-router';

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

export default router;

Removed APIs

Vue 2Vue 3 Alternative
$on, $off, $onceUse external library like mitt
FiltersMethods/computed
$childrenUse refs or provide/inject
$listenersMerged into $attrs
$scopedSlotsUnified as $slots
data as objectAlways function

Migration Checklist

  1. Update dependencies - Vue, Vue Router, Vuex, tooling
  2. Check breaking changes - Global API, v-model, filters
  3. Update build config - Webpack/Vite configuration
  4. Fix deprecation warnings - Use migration build
  5. Test thoroughly - Unit tests, E2E tests
  6. Gradual migration - Use migration build for large apps
  7. Remove compat mode - Once fully migrated

Quick Migration Script

# Install migration build
npm install @vue/compat

# Or upgrade all at once
npm install vue@next vue-router@4 vuex@4

# Use upgrade tool
npx @vue/upgrade next

Conclusion

Migrating to Vue 3 brings significant improvements in performance, TypeScript support, and developer experience. Use the migration build for gradual upgrades, and refer to the official migration guide for detailed information on each breaking change.

Share:

💬 Comments