Notable Breaking Changes
New Features
Composition API
Supporting Libraries
Migration Path
Additional Resources
Vue 2.x has a number of global APIs and configurations that globally mutate Vue’s behavior. For instance, to create a global component or directive, you would use the Vue.component and Vue.directive respectively.
Vue.component('button-counter', {
data: () => ({
count: 0
}),
template: `
<button @click="count++">
Clicked {{ count }} times.
</button>
`
})
Vue.directive('focus', {
inserted: el => el.focus()
})
Vue.component |
Vue.directive |
Vue.mixin |
Vue.use |
Vue.config |
Vue.use(MyPlugin)
Global configuration makes it difficult to share the same copy of Vue between multiple "apps" on the same page, but with different global configurations.
// this affects both root instances
Vue.mixin({
/* ... */
})
const app1 = new Vue({ el: '#app-1' })
const app2 = new Vue({ el: '#app-2' })
import { createLocalVue, mount } from '@vue/test-utils'
// create an extended `Vue` constructor
const localVue = createLocalVue()
// install a plugin “globally” on the “local” Vue constructor
localVue.use(MyPlugin)
// pass the `localVue` to the mount options
mount(Component, { localVue })
In addition, Global configuration makes it easy to accidentally pollute other test cases during testing.
import Vue from 'vue'
import Vuex from 'vuex'
import App from './app.vue'
Vue.use(Vuex)
new Vue({
render: (h) => h(App)
}).$mount('#app')
import { createApp } from 'vue'
import App from './App.vue'
import store from './store'
const app = createApp(App)
app.use(store)
app.mount('#app')
Vue 2.x Global API
Vue 3.x Instance API (app)
Vue.config |
Vue.config.productionTip |
Vue.config.ignoredElements |
Vue.component |
Vue.directive |
Vue.mixin |
Vue.use |
Vue.prototype |
app.config |
removed |
app.config.isCustomElement |
app.component |
app.directive |
app.mixin |
app.use |
app.config.globalProperties |
<!-- Layout.vue -->
<template>
<div>
<header>...</header>
<main>...</main>
<footer>...</footer>
</div>
</template>
<!-- Layout.vue -->
<template>
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
</template>
Vue 2.x
Vue 3.x
In Vue 3, components now have official support for multi-root node components.
Emitted events can be defined on the component via the emits option.
This is not mandatory but should be considered a best practice since it enables self-documenting code.
<template>
<div>
<p>{{ text }}</p>
<button v-on:click="$emit('accepted')">OK</button>
</div>
</template>
<script>
export default {
props: ['text'],
emits: ['accepted']
}
</script>
Sometimes a part of a component's template belongs to this component logically, while from a technical point of view, it would be preferable to move this part of the template somewhere else in the DOM, outside of the Vue app.
Teleport provides a clean way to allow us to control under which parent in our DOM we want a piece of HTML to be rendered, without having to resort to global state or splitting this into two components.
app.component('modal-button', {
template: `
<button @click="modalOpen = true">
Open full screen modal! (With teleport!)
</button>
<teleport to="body">
<div v-if="modalOpen" class="modal">
<div>
I'm a teleported modal!
(My parent is "body")
<button @click="modalOpen = false">
Close
</button>
</div>
</div>
</teleport>
`,
data() {
return {
modalOpen: false
}
}
})
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.
<template v-for="item in list" :key="item.id">
<div>...</div>
</template>
<div v-if="condition" key="yes">Yes</div>
<div v-else key="no">No</div>
<template v-for="item in list">
<div :key="item.id">...</div>
<span :key="item.id">...</span>
</template>
<template v-for="item in list" :key="item.id">
<div>...</div>
<span>...</span>
</template>
Vue 2.x
Vue 3.x
In Vue 3, the key should be placed on the <template> tag instead of each child.
<div v-if="condition" key="yes">Yes</div>
<div v-else key="no">No</div>
<div v-if="condition">Yes</div>
<div v-else>No</div>
Vue 2.x
Vue 3.x
In Vue 3, unique keys are now automatically generated on conditional branches if you don't provide them.
<template v-if="list.length" v-for="item in list" :key="item.id">
<div>...</div>
</template>
In Vue 2, functional components had two primary use cases:
However, in Vue 3, the performance of stateful components has improved to the point that the difference is negligible. In addition, stateful components now also include the ability to return multiple root nodes.
<template functional>
<component
:is="`h${props.level}`"
v-bind="attrs"
v-on="listeners"
/>
</template>
<script>
export default {
props: ['level']
}
</script>
In 3.x, the performance difference between stateful and functional components has been drastically reduced and will be insignificant in most use cases. As a result, the migration path for developers using functional on SFCs is to remove the attribute and rename all references of props to $props and attrs to $attrs.
<template>
<component
v-bind:is="`h${$props.level}`"
v-bind="$attrs"
/>
</template>
<script>
export default {
props: ['level']
}
</script>
Now in Vue 3, all functional components are created with a plain function. In other words, there is no need to define the { functional: true } component option.
They will receive two arguments: props and context. The context argument is an object that contains a component's attrs, slots, and emit properties.
In addition, rather than implicitly provide h in a render function, h is now imported globally.
import { h } from 'vue'
const DynamicHeading = (props, ctx) => {
return h(`h${props.level}`, ctx.attrs, ctx.slots)
}
DynamicHeading.props = ['level']
export default DynamicHeading
const asyncPage = () => import('./NextPage.vue')
const asyncPage = {
component: () => import('./NextPage.vue'),
delay: 200,
timeout: 3000,
error: ErrorComponent,
loading: LoadingComponent
}
Vue 2.x
Previously, async components were created by simply defining a component as a function that returned a promise.
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
const asyncPage = () => import('./NextPage.vue')
const asyncPage = {
component: () => import('./NextPage.vue'),
delay: 200,
timeout: 3000,
error: ErrorComponent,
loading: LoadingComponent
}
Vue 2.x
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
})
Vue 3.x
this.$scopedSlots removed and they are now unified into the $slots option.
In Vue 2, you can access attributes passed to your components with this.$attrs, and event listeners with this.$listeners. In combination with inheritAttrs: false, they allow the developer to apply these attributes and listeners to some other element instead of the root element.
<template>
<label>
<input type="text" v-bind="$attrs" v-on="$listeners" />
</label>
</template>
<script>
export default {
inheritAttrs: false
}
</script>
In Vue 3, event listeners are now just attributes, prefixed with on, and as such are part of the $attrs object, so $listeners has been removed.
<template>
<label>
<input type="text" v-bind="$attrs" />
</label>
</template>
<script>
export default {
inheritAttrs: false
}
</script>
Vue 2.x
Vue 3.x
In fact $attrs now contains all attributes passed to a component, including class and style.
<template>
<label>
<input type="text" v-bind="$attrs" />
</label>
</template>
<script>
export default {
inheritAttrs: false
}
</script>
Vue 3.x
<my-component id="my-id" class="my-class"></my-component>
<label>
<input type="text" id="my-id" class="my-class" />
</label>
<script>
export default {
beforeDestroy() {
console.log('beforeDestroy has been called')
}
destroyed() {
console.log('destroyed has been called')
}
}
</script>
Vue 2.x
Vue 3.x
The beforeDestroy & destroyed lifecycle hooks has been renamed to beforeUnmount & unmounted
<script>
export default {
beforeUnmount() {
console.log('beforeUnmount has been called')
}
unmounted() {
console.log('unmounted has been called')
}
}
</script>
Vue 2.x
Vue 3.x
The hook functions for directives have been renamed to better align with the component lifecycle.
Vue 2.x
The hook functions for directives have been renamed to better align with the component lifecycle.
created | |
bind | beforeMount |
inserted | mounted |
beforeUpdate | |
update | |
componentUpdated | updated |
beforeUnmount | |
unbind | unmounted |
Vue 3.x
.v-enter,
.v-leave-to {
opacity: 0;
}
.v-leave,
.v-enter-to {
opacity: 1;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
.v-leave-from,
.v-enter-to {
opacity: 1;
}
Vue 2.x
Vue 3.x
The v-enter transition class has been renamed to v-enter-from and
the v-leave transition class has been renamed to v-leave-from.
.v-enter
.v-enter-active
.v-enter-to
.v-enter-from
.v-enter-active
.v-enter-to
In 2.x, Vue instance could be used create global event listeners used across the whole application via the event emitter API ($on, $off and $once)
// eventBus.js
import Vue from 'vue'
const eventBus = new Vue()
export default eventBus
// ChildComponent.vue
import eventBus from './eventBus'
export default {
mounted() {
// adding eventBus listener
eventBus.$on('custom-event', () => {
console.log('Custom event triggered!')
})
},
beforeDestroy() {
// removing eventBus listener
eventBus.$off('custom-event')
}
}
// ParentComponent.vue
import eventBus from './eventBus'
export default {
methods: {
callGlobalCustomEvent() {
eventBus.$emit('custom-event')
}
}
}
In 3.x, $on, $off and $once instance methods are removed. Application instances no longer implement the event emitter interface. Functionality should be replaced by a 3rd party library like mitt or tiny-emitter.
<template>
<h1>Bank Account Balance</h1>
<p>{{ accountBalance | currencyUSD }}</p>
</template>
<script>
export default {
props: {
accountBalance: {
type: Number,
required: true
}
},
filters: {
currencyUSD(value) {
return '$' + value
}
}
}
</script>
<template>
<h1>Bank Account Balance</h1>
<p>{{ accountInUSD }}</p>
</template>
<script>
export default {
props: {
accountBalance: {
type: Number,
required: true
}
},
computed: {
accountInUSD() {
return '$' + this.accountBalance
}
}
}
</script>
Vue 2.x
Vue 3.x
Filters are removed from Vue 3.0 and no longer supported.
They should be replaced with computed properties or methods.
// src/components/UserRepositories.vue
export default {
components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: {
type: String,
required: true
}
},
data () {
return {
repositories: [],
searchQuery: ''.,
filters: { ... },
}
},
computed: {
filteredRepositories () { ... },
repositoriesMatchingSearchQuery () { ... },
},
watch: {
user: 'getUserRepositories'
},
methods: {
getUserRepositories () {
// using `this.user` to fetch user repositories
},
updateFilters () { ... },
},
mounted () {
this.getUserRepositories()
}
}
// ConsumingComponent.js
import MyMixin from "./MyMixin.js";
export default {
mixins: [MyMixin],
data: () => ({
myLocalDataProperty: null
}),
methods: {
myLocalMethod () { ... }
}
}
// MyMixin.js
export default {
data: () => ({
mySharedDataProperty: null
}),
methods: {
mySharedMethod () { ... }
}
}
export default {
data: () => ({
mySharedDataProperty: null
myLocalDataProperty: null
}),
methods: {
mySharedMethod () { ... },
myLocalMethod () { ... }
}
}
Options API
Composition API
//Counter.vue
export default {
data() {
return {
count: 0
}
},
computed: {
double () {
return this.count * 2;
}
},
methods: {
increment() {
this.count++;
}
}
}
// Counter.vue
import { ref, computed } from "vue";
export default {
setup() {
const count = ref(0);
const double = computed(() => count.value * 2)
function increment() {
count.value++;
}
return {
count,
double,
increment
}
}
}
// useCounter.js
import { ref, computed } from "vue";
export default function () {
const count = ref(0);
const double = computed(() => count.value * 2)
function increment() {
count.value++;
}
return {
count,
double,
increment
}
}
// Counter.vue
import { ref, computed } from "vue";
export default {
setup() {
const count = ref(0);
const double = computed(() => count.value * 2)
function increment() {
count.value++;
}
return {
count,
double,
increment
}
}
}
Composition API
Composition Function
// useCounter.js
import { ref, computed } from "vue";
export default function () {
const count = ref(0);
const double = computed(() => count.value * 2)
function increment() {
count.value++;
}
return {
count,
double,
increment
}
}
// MyComponent.js
import useCounter from "./useCounter.js";
export default {
setup() {
const { count, double, increment } = useCounter();
return {
count,
double,
increment
}
}
}
Composition API
Composition Function
// useCounter.js
import { ref, computed } from "vue";
export default function () {
const count = ref(0);
const double = computed(() => count.value * 2)
function increment() {
count.value++;
}
return {
count,
double,
increment
}
}
// MyComponent.js
import useCounter from "./useCounter.js";
export default {
setup() {
return {
...useCounter()
}
}
}
Composition API
Composition Function
import { ref, watch } from 'vue'
const counter = ref(0)
watch(counter, (newValue, oldValue) => {
console.log('The new counter value is: ' + counter.value)
})
Reacting to Changes with watch
import { fetchData } from '@/api'
import { ref, onMounted } from 'vue'
// in our component
setup (props) {
const todos = ref([])
const getData = async () => {
todos.value = await fetchData(props.user)
}
onMounted(getData)
return {
todos
}
}
Lifecycle hooks
Vue CLI - As of v4.5.0, vue-cli now provides the built-in option to choose Vue 3 when creating a new project. You can upgrade vue-cli and run vue create to create a Vue 3 project today.
Vuex - Vuex 4.0 provides Vue 3 support with largely the same API as 3.x. The only breaking change is how the plugin is installed.
Vue Router - Vue Router 4.0 provides Vue 3 support and has a number of breaking changes of its own.
Devtools Extension - The new version is currently in beta and only supports Vue 3 (for now). Vuex and Router integration is also work in progress.
Vetur - VSCode official extension Vetur provides comprehensive IDE support for Vue 3.
Ionic Vue - Ionic Vue is built on top of Vue 3.x If you've built an app with early versions of Ionic Vue, you'll want to upgrade to the latest release and upgrade your Vue dependencies.
Need to add an extra polyfill for proxies in order to support IE
* 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.