Refactoring Vue 2 Apps to Vue 3

A complete guide to upgrading your Vue 2 applications to Vue 3 with minimal disruption. Covers global API changes, Composition API migration, and state management updates.

Why Migrate: Understanding the Stakes

Vue 2 reached end-of-life on December 31, 2023, meaning no more security patches or ecosystem updates. For development teams maintaining Vue 2 applications, the question is no longer whether to migrate--but how to do it effectively.

Security and Compliance Considerations

Without official security support, any vulnerabilities discovered in Vue 2 remain unpatched. For applications handling sensitive user data or requiring GDPR compliance, this creates unacceptable risk. The average cost of a data breach continues to rise, making proactive framework updates a security investment rather than optional maintenance.

Ecosystem Isolation

The Vue ecosystem has decisively moved forward. Modern tools and libraries--Vuetify 3, Pinia, Vue Router 4, Vite, and Vitest--are designed exclusively for Vue 3. Remaining on Vue 2 means accepting version conflicts, outdated dependencies, and missing features like script setup syntax and Suspense for async loading.

Our web development team has extensive experience with framework migrations and can help you navigate this transition smoothly while maintaining business continuity.

Vue 3 Performance Improvements

10-20%

Faster reactivity with Proxy-based approach

18-30%

Bundle size reduction on average

40%

Faster build times with Vite

Global API Restructuring: The Foundation Change

The most fundamental change in Vue 3 is how the framework is initialized and mounted. Every Vue 2 application uses patterns that must be completely refactored.

From Vue.use() to createApp()

In Vue 2, plugins and component registration happened globally using Vue.use(), Vue.component(), and new Vue(). Vue 3 introduces a completely different initialization pattern with createApp():

// Vue 3 approach
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.component('BaseIcon', BaseIconComponent)
app.mount('#app')

The key differences are significant: createApp returns an application instance rather than the Vue constructor. Each application instance is isolated, meaning component registrations and plugin installations in one Vue 3 app won't affect another. This improves modularity and makes testing cleaner.

For teams working on complex full-stack JavaScript applications, the isolation provided by Vue 3's application instances simplifies testing and maintenance.

Vue 2 to Vue 3 Global API Changes
Vue 2 (Global)Vue 3 (Instance)
Vue.use()app.use()
Vue.component()app.component()
Vue.directive()app.directive()
Vue.mixin()app.mixin()
Vue.configapp.config
Vue.prototypeapp.config.globalProperties
Vue.extend()app.component()

Composition API: The Architectural Shift

Vue 3 introduces the Composition API as an alternative to the Options API. While Options API remains fully supported, Composition API offers significant advantages for complex components and code organization.

Benefits of Composition API

  • Better code organization: Group logic by feature rather than by option type
  • Improved TypeScript support: Native typing without additional configuration
  • Flexible logic reuse: Composables replace mixins with explicit dependencies
  • Smaller production bundles: Tree-shaking removes unused code

Extract Logic into Composables

One of Composition API's biggest benefits is the ability to extract and reuse logic across components:

// composables/useUserApi.js
import { ref } from 'vue'
import { api } from '@/services/api'

export function useUserApi() {
 const user = ref(null)
 const loading = ref(false)
 const error = ref(null)
 
 async function fetchUser(userId) {
 loading.value = true
 error.value = null
 try {
 user.value = await api.getUser(userId)
 } catch (e) {
 error.value = e.message
 } finally {
 loading.value = false
 }
 }
 
 async function logout() {
 user.value = null
 await api.logout()
 }
 
 return { user, loading, error, fetchUser, logout }
}

Composables replace Vue 2's mixins with a clearer, more explicit pattern. Unlike mixins, composables clearly show their dependencies and avoid naming conflicts. This pattern aligns with modern JavaScript development practices and improves maintainability across your JavaScript applications.

Vue 2 Options API
1export default {2 props: {3 userId: {4 type: String,5 required: true6 }7 },8 9 data() {10 return {11 user: null,12 loading: false,13 error: null14 }15 },16 17 computed: {18 isAuthenticated() {19 return this.user && this.user.token20 }21 },22 23 methods: {24 async fetchUser() {25 this.loading = true26 try {27 this.user = await api.getUser(this.userId)28 } catch (e) {29 this.error = e.message30 } finally {31 this.loading = false32 }33 }34 },35 36 created() {37 this.fetchUser()38 }39}
Vue 3 Composition API
1<script setup>2import { ref, computed, onMounted } from 'vue'3import { useUserApi } from '@/composables/useUserApi'4 5const props = defineProps({6 userId: {7 type: String,8 required: true9 }10})11 12const { user, loading, error, fetchUser } = useUserApi()13 14const isAuthenticated = computed(() => 15 user.value && user.value.token16)17 18onMounted(() => {19 fetchUser(props.userId)20})21</script>

v-model and Template Directive Changes

Vue 3 refines how two-way binding works, with v-model replacing the combination of Vue 2's v-model and :value.sync patterns. Understanding these changes is essential for maintaining functionality in your JavaScript applications during migration.

v-model Changes

In Vue 2, v-model was essentially :value + @input. Vue 3 consolidates this with a clearer modelValue prop:

// Vue 2 component definition
export default {
 model: {
 prop: 'value',
 event: 'input'
 },
 props: {
 value: String
 }
}

// Vue 3 component definition
export default {
 props: {
 modelValue: String
 },
 emits: ['update:modelValue']
}

v-for and key Usage

Vue 3 changes the precedence of v-for and v-if. In Vue 2, v-for took precedence. Vue 3 reverses this, so use template tags for clarity:

<!-- Vue 3: v-if takes precedence -->
<template v-for="item in items" :key="item.id">
 <li v-if="item.active">
 {{ item.name }}
 </li>
</template>

Event Handling Changes

The .native modifier for events has been removed in Vue 3. Native events on components now bubble naturally. The $listeners object has been merged into $attrs.

State Management: Vuex to Pinia

Pinia is now Vue's recommended state management library, replacing Vuex with a simpler, type-safe API.

Migration Benefits

  • Simpler API: No mutations, getters, or modules structure
  • TypeScript support: Built-in TypeScript support without decorators
  • No boilerplate: Actions are plain functions
  • Devtools integration: Excellent debugging experience

Example Migration

A Vuex store converts to Pinia by defining state as refs and computed as computed properties:

// Pinia store example
export const useUserStore = defineStore('user', () => {
 const user = ref(null)
 
 const isAuthenticated = computed(() => !!user.value)
 
 async function login(credentials) {
 const userData = await api.login(credentials)
 user.value = userData
 return userData
 }
 
 return { user, isAuthenticated, login }
})

Migrating to Pinia as part of your Vue 3 upgrade provides a more maintainable state management solution that scales with your application. This is one of the key improvements in our full-stack development services.

Using the Migration Build (@vue/compat)

Vue 3 provides a migration build that can run Vue 2 code with Vue 3, providing warnings for deprecated patterns. This allows gradual refactoring without complete rewrites.

Setting Up Migration Build

Install the migration build alongside Vue 3 and configure your build to use it:

import { configureCompat } from '@vue/compat'

const compatConfig = {
 MODE: 2,
 GLOBAL_EXTEND: false,
 GLOBAL_OBSERVABLE: false,
}

configureCompat(compatConfig)

Migration Build Strategy

  1. Run the application in compatibility mode with warnings enabled
  2. Address each warning systematically
  3. Disable compat flags for specific features once migrated
  4. Eventually remove the migration build entirely

This approach minimizes risk by allowing feature development to continue during migration.

For organizations planning a smooth transition, our web development team can help implement a phased migration strategy that keeps your applications running while upgrading to Vue 3.

Common Migration Questions

Conclusion

Migrating from Vue 2 to Vue 3 is a significant undertaking, but the benefits--security, performance, ecosystem access, and developer experience--make it essential for long-term application health. Start by understanding the global API changes, then systematically refactor components to use Composition API patterns. Use the migration build for gradual transitions, and don't forget to migrate state management to Pinia. With proper planning and incremental execution, Vue 3 migration can be completed without disrupting feature development.

Key Takeaways:

  • Vue 2 EOL means no more security updates--migration is now a necessity
  • The global API change from Vue.use() to createApp() is the foundational shift
  • Composition API offers better organization and TypeScript support
  • Pinia replaces Vuex with a simpler, type-safe API
  • The migration build enables gradual, low-risk transitions

If you're planning a Vue migration, our experienced development team can help assess your application and create a migration strategy that minimizes risk and disruption.


Sources

  1. Vue.js Official Migration Guide
  2. Vue.js Global API Breaking Changes
  3. Vue.js Breaking Changes Index

Need Help with Your Vue Migration?

Our team has extensive experience migrating Vue applications. We can help you plan and execute a smooth transition to Vue 3.