Vue 3 — New features, Breaking changes & a Migration path

Fotis Adamakis
Vue.js Developers
Published in
7 min readSep 18, 2020

--

Vue 3 is here and everyone is looking for a way to migrate and start using it as soon as possible. There are several new features but also a lot of work done to improve performance and bundle size under the hood that makes this version a real candidate for the best client-side framework out there. The catch? New syntax, deprecations, and some breaking changes might make your migration plan slightly harder than expected. Let’s dive in and see what you should expect.

Mounting

The first thing that you will encounter is the difference in initializing your app. In Vue 2 you have to use Vue constructor with a render function and the $mount method like this

import Vue from 'vue'import App from './app.vue'

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

In Vue 3 this is simplified with a more elegant syntax

import { createApp } from "vue";import App from "./App.vue";createApp(App).mount("#app");

This will not make much difference in our app since most of the times only one Vue instance is created, but it will make a big difference in our tests. Until now we had to use the localVue pattern in order to keep each test in isolation with the others which is no longer needed.

Fragments

In Vue 2, multi-root components were not supported. The solution was to enclose your code in a wrapper element.

<!-- Layout.vue -->
<template>
<div>
<header>...</header>
<main>...</main>
<footer>...</footer>
</div>
</template>

In Vue 3, components now can have multiple root nodes. This enables eliminating wrapper elements and writing cleaner markup.

<!-- Layout.vue -->
<template>
<header>...</header>
<main>...</main>
<footer>...</footer>
</template>

Teleport

A not so common problem but very difficult to solve is having part of your component mounted in a different position in DOM than the Vue component hierarchy.

A common scenario for this is creating a component that includes a full-screen modal. In most cases, you’d want the modal’s logic to live within the component, but the positioning of the modal quickly becomes difficult to solve through CSS, or requires a change in component composition.

This can now easily achieved with the use of the teleport feature like this

app.component('app-modal', {
template: `
<button @click="isOpen = true">
Open modal
</button>
<teleport to="body">
<div v-if="isOpen" class="modal">
I'm a teleported modal
</div>
</teleport>
`,
data() {
return {
isOpen: false
}
}
})

You can still interact and pass props to it like being inside the component!

Emits

How you emit events hasn’t changed but you can declare the emits in your component like this

export default {
name: 'componentName',
emits: ['eventName']
}

This is not mandatory but should be considered a best practice because it enables self-documenting code

Key Attributes

The key special attribute is used as a hint for Vue’s virtual DOM algorithm to keep track of a node’s identity. That way, Vue knows when it can reuse and patch existing nodes and when it needs to reorder or recreate them.

We have 2 changes in this. First of all keys in template loops don’t need to repeat for every child element.

Vue 2.x
<template v-for="item in list">
<div :key="item.id">...</div>
<span :key="item.id">...</span>
</template>
Vue 3.x
<template v-for="item in list" :key="item.id">
<div>...</div>
<span>...</span>
</template>

And secondly, in conditional branches, the key attribute is now added automatically.

Vue 2.x
<div v-if=”condition” key=”yes”>Yes</div>
<div v-else key=”no”>No</div>
Vue 3.x
<div v-if="condition">Yes</div>
<div v-else>No</div>

Composition API

A very controversial topic when first introduced back in June 2019 was the new Function-based Component API. This is a lot different than the existing Options API and caused a lot of confusion on the first sight. The good thing is that the existing Options API is not deprecated and everything is purely additive for handling advanced use cases and mainly replace mixins usage that admittedly has caused a lot of problems in large scale applications.

The new Composition API was designed for logic organization, encapsulations, and reuse which makes it extremely flexible, performant (no component instances involved) and makes easier tracking the source of every component property.

A simple example of how a component is structured by using the new API is the following

<template>
<button @click="increment">
Count is: {{ state.count }}, double is: {{ state.double }}
</button>
</template>

<script>
import { reactive, computed } from 'vue'

export default {
setup() {
const state = reactive({
count: 0,
double: computed(() => state.count * 2)
})

function increment() {
state.count++
}

return {
state,
increment
}
}
}
</script>

The main drawback is that it will require some extra time to get familiar with it which doesn’t really align with the easy learning curve that Vue 2 is known. The good thing is that you don’t need to rewrite your existing components using the new API and you don’t need to use it everywhere as well.

Functional Components

Functional components are deprecated. The main reason to use a functional component was performance which is now no longer relevant since the changes done under the hood in component instantiation and compilation make this difference insignificant. This change unfortunately will require some manual migration.

Scoped slots

A change that might be painful for you to refactor if you use them is the removal of scoped slots. They are now merged with slots.

// Vue 2 Syntax
this.$scopedSlots.header

// Vue 3 Syntax
this.$slots.header()

Event Bus

$on, $once, and $off methods are removed from the Vue instance, so in Vue 3 it can’t be used to create an event bus. Vue docs recommend using mitt library. It’s tiny and has the same API as Vue 2.

Filters

In Vue 3 filters are removed! You can actually implement the same functionality in a small plugin but the fact that the pipe of the filter conflicts with the Javascript bitwise operator means that expressions with filters are not valid. That's why the recommendation is using a method instead.

// Vue 2 Syntax
{{ msg | format }}
// Vue 3 Alternative
{{ format(msg) }}

The drawback of this is that chaining multiple methods is not that elegant as chaining multiple filters but that’s a small price to pay.

// Vue 2 Syntax
msg | uppercase | reverse | pluralize
// Vue 3 Alternative
pluralize(reverse(uppercase(msg)))

Async components

Previously, async components were created by simply defining a component as a function that returned a promise.

const asyncPage = () => import('./NextPage.vue')const asyncPage = {
component: () => import('./NextPage.vue'),
delay: 200,
timeout: 3000,
error: ErrorComponent,
loading: LoadingComponent
}

Now, in Vue 3, since functional components are defined as pure functions, async components definitions need to be explicitly defined by wrapping it in a new defineAsyncComponent helper

import { defineAsyncComponent } from 'vue'// Async component without options
const asyncPage = defineAsyncComponent(() => import('./NextPage.vue'))
// Async component with options
const asyncPageWithOptions = defineAsyncComponent({
loader: () => import('./NextPage.vue'),
delay: 200,
timeout: 3000,
errorComponent: ErrorComponent,
loadingComponent: LoadingComponent
})

Component lifecycle hooks

The beforeDestroy & destroyed lifecycle hooks have been renamed to beforeUnmount & unmounted

Vue 2.x
<script>
export default {
beforeDestroy() {
console.log('beforeDestroy has been called')
}
destroyed() {
console.log('destroyed has been called')
}
}
</script>
Vue 3.x
<script>
export default {
beforeUnmount() {
console.log('beforeUnmount has been called')
}
unmounted() {
console.log('unmounted has been called')
}
}
</script>

IE11 support

IE11 is not supported from the main bundle. If you are unlucky enough to have to support it, you will have to include some additional files with your bundle to polyfill things like proxies that are used from Vue 3.

Vuex

Vuex 4 has also released to accompany Vue 3. The API remains the same and the code will be compatible with the previous version. Disappointed? You shouldn’t be! That’s one less thing to migrate and with Vuex 5 just around the corner be sure that changes are coming. Removal of Mutations and nested modules only to name a few.

Migration plan to Vue 3

  1. Read the official migration guide
  2. Replace Event bus usages with mitt library
  3. Update scoped slots to be regular slots
  4. Replace filter with methods
  5. Upgrade to Vue 2.7 — This version will have deprecation warnings for every feature that is not compatible with Vue 3 and will guide you with documentation links on how to handle every case.
  6. Upgrade to Vue 3

Just have in mind that this will probably be a long process and might take up to one year, depending on your project size and the deprecated features you are currently using. It might not be your first priority but given the massive performance improvement and the elegant new Composition API, this is definitely worth it!

--

--

Fotis Adamakis
Vue.js Developers

« Senior Software Engineer · Author · International Speaker · Vue.js Athens Meetup Organizer »