Next.js starter your AI actually understands. Ship internal tools in days not weeks. Pre-order $199 $499 → [Get it now]

How to migrate Vue 2 to Vue 3

Migrating from Vue 2 to Vue 3 requires addressing breaking changes in the API, removed features, and updated behavior. As the creator of CoreUI with over 10 years of Vue.js experience since 2014, I’ve led Vue 2 to Vue 3 migrations for large applications and open-source libraries. The most effective approach uses the official migration build to run Vue 3 with Vue 2 compatibility flags, then addresses warnings one by one. This incremental strategy avoids a big-bang rewrite.

Start with the Vue 3 migration build to identify issues.

npm install vue@^3 @vue/compat

# Replace vue-router and vuex with latest versions
npm install vue-router@^4 vuex@^4
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue({
    template: {
      compilerOptions: {
        compatConfig: { MODE: 2 }
      }
    }
  })],
  resolve: {
    alias: {
      vue: '@vue/compat'
    }
  }
})

@vue/compat runs Vue 3 with Vue 2 compatibility mode. MODE: 2 enables maximum compatibility. The build logs deprecation warnings for each Vue 2 pattern. Fix warnings iteratively until you can disable compat mode entirely.

Replacing Filters

Vue 3 removed filters - use methods or computed instead.

// ❌ Vue 2 - filters
<template>
  <p>{{ price | currency }}</p>
  <p>{{ date | formatDate }}</p>
</template>

filters: {
  currency(value) {
    return '$' + value.toFixed(2)
  }
}
// ✅ Vue 3 - computed or methods
<template>
  <p>{{ formattedPrice }}</p>
  <p>{{ formatDate(date) }}</p>
</template>

<script setup>
import { computed } from 'vue'

const props = defineProps({ price: Number, date: String })

const formattedPrice = computed(() => '$' + props.price.toFixed(2))
const formatDate = (d) => new Date(d).toLocaleDateString()
</script>

Filters were syntactic sugar for function calls. Replace them with computed properties for reactive transformations or methods for on-demand formatting. The behavior is identical.

Replacing Event Bus

Vue 3 removed $on, $off, and $emit on the root instance.

// ❌ Vue 2 - event bus via Vue instance
const EventBus = new Vue()
EventBus.$emit('event', data)
EventBus.$on('event', callback)
// ✅ Vue 3 - use mitt or custom event bus
import mitt from 'mitt'

const emitter = mitt()
export default emitter

// Publisher
import emitter from './emitter'
emitter.emit('user:login', { id: 1 })

// Subscriber
import emitter from './emitter'
emitter.on('user:login', (user) => console.log(user))

Vue 3 removed the instance event bus. Replace with mitt (tiny library) or a custom pub/sub class. The pattern is the same, just without Vue dependency.

Updating Vue Router

Vue Router 4 has breaking changes in navigation guards.

// ❌ Vue 2 Router - next() required
router.beforeEach((to, from, next) => {
  if (!isAuthenticated) {
    next('/login')
  } else {
    next()
  }
})

// ✅ Vue Router 4 - return value
router.beforeEach((to, from) => {
  if (!isAuthenticated) {
    return '/login'
  }
})

Navigation guards no longer require calling next(). Return a route location to redirect. Return false to cancel navigation. Return undefined or true to proceed. This simpler API reduces errors.

Updating Vuex to Pinia

Migrate Vuex 4 stores to Pinia for better TypeScript support.

// Vuex store
const store = createStore({
  state: () => ({ count: 0 }),
  mutations: {
    increment(state) { state.count++ }
  },
  actions: {
    asyncIncrement({ commit }) {
      setTimeout(() => commit('increment'), 1000)
    }
  }
})
// Pinia equivalent
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  actions: {
    increment() { this.count++ },
    asyncIncrement() {
      setTimeout(() => this.increment(), 1000)
    }
  }
})

Pinia removes the mutations/actions split - everything is an action. this refers to the store state directly. TypeScript inference works without manual type declarations. Pinia is the officially recommended replacement.

Best Practice Note

This is the same migration strategy we used to upgrade CoreUI Vue from version 2 to version 3. Use @vue/compat migration build to discover issues incrementally rather than rewriting everything at once. Read the official Vue 3 migration guide for a complete list of breaking changes. Upgrade one component at a time starting with leaf components that have no children. The Composition API migration can happen separately from the Vue 3 upgrade - Options API still works in Vue 3.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author