iTranslated by AI
Migrating a Vue 2 Project to Vue 3
About a year has passed since Vue 3 was officially released.
With Vuetify's release goal of Q3 2021 approaching, are you considering updating to Vue 3 soon?
In this article, I will describe the steps to migrate from Vue 2 to Vue 3.
I have prepared the following repository as a reference project.
If you want to experience the migration from Vue 2, please check out the vue2-todo-app tag.
Using the Migration Build
Official tools provide @vue/compat for migrating from Vue 2 to Vue 3.
Using @vue/compat allows the application to run in Vue 2 mode, so you can continue to use APIs that were removed or deprecated in Vue 3, with some exceptions. For features that are removed or deprecated in Vue 3, warnings will be output at runtime. This behavior can also be configured to be enabled or disabled on a per-component basis.
The migration build is scheduled to be discontinued in a future minor version (after the end of 2021), so you should aim to switch to the standard build by then.
Also, if the application is large and complex, migration might be difficult even with the migration build. Since the Composition API and other Vue 3 features are expected to be available in the version 2.7 release (scheduled for the latter half of Q3 2021), staying on Vue 2 is also an option.
Cautions
Currently, the following limitations exist, so it may not be possible to apply the migration tool.
- Dependency on component libraries such as Vuetify, Quasar, or ElementUI. You will need to wait for a version compatible with Vue 3 to be released.
- IE11 support. Vue 3 has discontinued support for IE11, so if you need IE11 support, you will likely be unable to migrate to Vue 3.
- Nuxt.js. Wait for the release of Nuxt 3.
Performing the Upgrade
Now, let's go through the actual steps of migrating a Vue 2 application to Vue 3.
Removing Deprecated Slot Syntax
As a step before starting the migration process, you need to remove the slot syntax that was deprecated in version 2.6.0.
- src/components/TodoForm.vue
<app-button>
- <template slot="text">更新する</template>
+ <template v-slot:text>更新する</template>
</app-button>
Installing Tools
Upgrade the tools as necessary.
In the reference project, since vue-cli is used, we upgrade to the latest @vue/cli-service using vue upgrade.
$ vue upgrade
In this case, since the latest vue-cli was already being used, there were no particular differences.
Next, install the following package versions:
-
vue=> 3.1 -
@vue/compat=> 3.1 (the same version asvue) -
vue-template-compiler=> Replace with@vue/compiler-sfc@3.1.0
$ npm i vue@3.1 @vue/compat@3.1
$ npm i -D @vue/compiler-sfc@3.1.0
$ npm uninstall vue-template-compiler -D
Modifying Build Settings
In the build settings, alias vue to @vue/compat and enable the compat mode in the Vue compiler options.
The example for vue-cli is as follows:
- vue.config.js
// vue.config.js
module.exports = {
chainWebpack: config => {
config.resolve.alias.set('vue', '@vue/compat')
config.module
.rule('vue')
.use('vue-loader')
.tap(options => {
return {
...options,
compilerOptions: {
compatConfig: {
MODE: 2
}
}
}
})
}
}
Fixing Compilation Errors
After following the steps so far, you may encounter many compilation errors.
Let's fix them one by one.
Fixing ESLint
Install version 7.x of eslint-plugin-vue and change the configuration to 'plugin:vue/vue3-recommended'.
$ npm i eslint-plugin-vue@7 -D
- .eslintrc.js
extends: [
- "plugin:vue/essential",
+ "plugin:vue/vue3-recommended",
"eslint:recommended",
"@vue/typescript/recommended",
"@vue/prettier",
"@vue/prettier/@typescript-eslint",
],
Let's run the lint. Some unfixable errors will be output, but we will fix them later.
$ npm run lint
Some items were automatically fixed by the lint.
For example, the .sync modifier was removed and replaced with v-model.
Component v-model has been reworked, replacing v-bind.sync
- src/components/EditTodo.vue
<todo-form
@submit="onSubmit"
- :title.sync="title"
- :description.sync="description"
- :status.sync="status"
+ v-model:title="title"
+ v-model:description="description"
+ v-model:status="status
/>
Upgrading Vue Router
If you are using vue-router, upgrade it to v4.
npm i vue-router@4
Let's fix the various compilation errors that occurred as a result of upgrading vue-router.
new VueRouter => createRouter
VueRouter is no longer a class, so use createRouter instead of new VueRouter().
- src/router/index.ts
- import VueRouter, { RouteConfig } from "vue-router";
+ import { createRouter } from "vue-router";
// Omitted
- const router = new VueRouter({
+ const router = createRouter({
mode: "history",
base: process.env.BASE_URL,
routes,
});
mode: history => history: createWebHistory()
The mode option has been removed and replaced with history. If you specified history in mode, use createWebHistory().
Also, the base option is now passed as an argument to createWebHistory().
- src/router/index.ts
- import { createRouter } from "vue-router";
+ import { createRouter, createWebHistory } from "vue-router";
// Omitted
const router = createRouter({
- mode: "history",
- base: process.env.BASE_URL,
+ history: createWebHistory(process.env.BASE_URL),
routes,
});
RouteConfig => RouteRecordRaw
This is simply a rename of the TypeScript type.
- src/router/index.ts
- import { createRouter, createWebHistory } from "vue-router";
+ import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
Vue.use(VueRouter);
- const routes: Array<RouteConfig> = [
+ const routes: Array<RouteRecordRaw> = [
Removing Vue.use(VueRouter)
Vue.use(VueRouter) is no longer necessary, so remove it. (We will replace it with the correct router registration method later.)
- src/router/index.ts
- import Vue from "vue";
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
- Vue.use(VueRouter);
Upgrading Vuex
Similarly, upgrade vuex to v4.
$ npm i vuex@4
Since the differences are fewer compared to vue-router, I will summarize them into one. Use createStore instead of new Vuex.Store and remove Vue.use(Vuex).
- src/store/index.ts
- import Vue from "vue";
- import Vuex from "vuex";
+ import { createStore } from "vuex";
import todos from "@/store/todos/index";
- Vue.use(Vuex);
export default createStore({
modules: {
todos,
},
});
Fixing the Vue Global API
One of the major changes in Vue 3 is that the Vue global instance has been deprecated.
For example, new Vue({}) is replaced by createApp, and Vue.use() is replaced by app.use() (where app is the instance created by createApp).
Additionally, Vue.config.productionTip has been removed.
Modifying main.ts
First, let's modify main.ts.
- src/main.ts (Before)
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
Vue.config.productionTip = false
new Vue({
router,
store,
render: (h) => h(App),
}).$mount("#app");
- src/main.ts (After)
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
const app = createApp(App)
app.use(router)
app.use(store)
app.mount("#app")
Since type errors will occur, let's also fix shims-vue.d.ts while we're at it.
- Before
declare module "*.vue" {
import Vue from "vue";
export default Vue;
}
- After
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent
export default component
}
Removing Vue.extend
If you were using the Options API with TypeScript, you used Vue.extend to provide type inference for components. This has also been removed. Use defineComponent instead.
It's quite a hassle because you have to replace it in every component...
- import Vue from "vue";
+ import { defineComponent } from "vue";
- export default Vue.extend({
+ export default defineComponent({
$store Type Definition
Since the type definition for this.$store is lost, create a vuex.d.ts file to provide a type definition for this.$store.
import { Store } from "vuex";
declare module "@vue/runtime-core" {
interface ComponentCustomProperties {
$store: Store;
}
}
Removing $listeners
The $listeners object has been removed in Vue 3 and has simply become part of the $attrs object.
- src/components/AppInput.vue
<template>
<label>
{{ label }}
<input id="input" v-bind="$attrs" />
</label>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
inheritAttrs: false,
props: {
label: {
type: String,
default: "",
},
},
});
</script>
Removing Filters
In Vue 3, filter syntax has been removed. Instead, replace them with simple methods or computed properties.
- src/components/TodoItem.vue (Before)
<template>
<div class="card">
<div>
<span class="title">
<router-link :to="`todos/${todo.id}`">{{ todo.title }}</router-link>
</span>
<span class="status" :class="todo.status">{{ todo.status }}</span>
</div>
<div class="body">Creation Date: {{ todo.createdAt | formatDate }}</div>
<hr />
<div class="action">
<button @click="clickDelete">Delete</button>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { Todo } from "@/repositories/TodoRepository/types";
export default defineComponent({
props: {
todo: {
type: Object as PropType<Todo>,
required: true,
},
},
filters: {
formatDate(date: Date): string {
return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
},
},
methods: {
clickDelete() {
this.$emit("delete", this.todo.id);
},
},
});
</script>
- src/components/TodoItem.vue (After)
<template>
<div class="card">
<div>
<span class="title">
<router-link :to="`todos/${todo.id}`">{{ todo.title }}</router-link>
</span>
<span class="status" :class="todo.status">{{ todo.status }}</span>
</div>
<div class="body">Creation Date: {{ formatDate }}</div>
<hr />
<div class="action">
<button @click="clickDelete">Delete</button>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { Todo } from "@/repositories/TodoRepository/types";
export default defineComponent({
props: {
todo: {
type: Object as PropType<Todo>,
required: true,
},
},
computed: {
formatDate(): string {
const { createdAt } = this.todo;
return `${createdAt.getFullYear()}/${
createdAt.getMonth() + 1
}/${createdAt.getDate()}`;
},
},
methods: {
clickDelete() {
this.$emit("delete", this.todo.id);
},
},
});
</script>
Using <template v-for>
VueCompilerError: <template v-for> key should be placed on the <template> tag.
In Vue 2, the key could not be included in the <template> tag, so keys were placed on each child element. In Vue 3, the key can now be included in the <template> tag, so we fix this as follows:
- src/pages/TodoList.vue
- <template v-for="(todo, index) in todos">
- <span :key="`index-${todo.id}`">{{ index + 1 }}</span>
- <todo-item :todo="todo" :key="`item-${todo.id}`" @delete="deleteTodo" />
+ <template v-for="(todo, index) in todos" :key="todo.id">
+ <span>{{ index + 1 }}</span>
+ <todo-item :todo="todo" @delete="deleteTodo" />
By this point, the application should be working without any problems! 🎉
Fixing Individual Warnings
While the application is running thanks to @vue/compat, to fully migrate to Vue 3, you also need to fix errors at the warning level.

As an example, let's fix the second one, deprecation COMPONENT_V_MODEL. This is a change in property and event names when using v-model with custom components.
- Property: value -> modelValue
- Event: input -> update:modelValue
I fixed it as follows:
- src/components/AppInput.vue
<template>
<label>
{{ label }}
<input
:value="modelValue"
@input="(e) => $emit('update:modelValue', e.target.value)"
/>
</label>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
props: {
modelValue: {
type: String,
default: "",
},
label: {
type: String,
default: "",
},
},
});
</script>
Then, a new warning is output.

This error occurs because we are using Vue 3 syntax while the application is running in Vue 2 mode. To remove this, modify the compatConfig option.
- src/components/AppInput.vue
import { defineComponent } from "vue";
export default defineComponent({
+ compatConfig: {
+ COMPONENT_V_MODEL: false,
+ },
props: {
modelValue: {
type: String,
default: "",
},
label: {
type: String,
default: "",
},
},
});
I added COMPONENT_V_MODEL: false to compatConfig. As shown above, it's possible to opt-in to Vue 3 behavior on a per-component basis.
You can also change this via global settings rather than per-component.
import { configureCompat } from 'vue'
// Disable compat for specific features
configureCompat({
FEATURE_ID_A: false,
FEATURE_ID_B: false
})
Complete Migration
Once you have removed all warnings, you can remove the migration tool!
$ npm uninstall @vue/compat
- vue.config.js
// vue.config.js
module.exports = {};
Discussion
vue upgrage -> vue upgrade
ですかね?
そのとおりでした、ご指摘ありがとうございます!