Hook functions for directives have been renamed to better align with the component lifecycle.
- created – new
- bind -> beforeMount
- inserted -> mounted
- beforeUpdate – new
- update – removed
- componentUpdated -> updated
- beforeUnmount – new
- unbind -> unmounted
Vue 2
<p v-highlight="'yellow'">Highlight this text bright yellow</p> // setup Vue.directive('highlight', { bind(el, binding, vnode) { el.style.background = binding.value } }) // accessing the component instance Vue.directive('some-directive', { bind(el, binding, vnode) { const vm = vnode.context } })
Vue 3
<p v-highlight="'yellow'">Highlight this text bright yellow</p> // setup const app = Vue.createApp({}) app.directive('highlight', { beforeMount(el, binding, vnode) { el.style.background = binding.value } }) // accessing the component instance app.directive('some-directive', { mounted(el, binding, vnode) { const vm = binding.instance } })
data component option declaration no longer accepts a plain JavaScript object and expects a function declaration.
Vue 2
const app = new Vue({ data: { apiKey: 'a1b2c3' } }) // or const app = new Vue({ data() { return { apiKey: 'a1b2c3' } } })
Vue 3
import { createApp } from 'vue' createApp({ data() { return { apiKey: 'a1b2c3' } } }).mount('#app')
When merging multiple data return values from mixins or extends, the merge is now shallow instead of deep (only root-level properties are merged).
Vue 2
const Mixin = { data() { return { user: { name: 'Jack', id: 1 } } } } const CompA = { mixins: [Mixin], data() { return { user: { id: 2 } } } } // result { "user": { "id": 2, "name": "Jack" } }
Vue 3
const Mixin = { data() { return { user: { name: 'Jack', id: 1 } } } } const CompA = { mixins: [Mixin], data() { return { user: { id: 2 } } } } // result { "user": { "id": 2, } }
Mounted application does not replace the element it’s mounted to.
Vue 2
// initial html <body> <div id="app"> Some app content </div> </body> // setup const app = new Vue({ data() { return { message: 'Hello Vue!' } }, template: ` <div id="rendered">{{ message }}</div> ` }) app.$mount('#app') <!-- result --> <body> <div id="rendered">Hello Vue!</div> </body>
Vue 3
// initial html <body> <div id="app"> Some app content </div> </body> // setup const app = Vue.createApp({ data() { return { message: 'Hello Vue!' } }, template: ` <div id="rendered">{{ message }}</div> ` }) app.mount('#app') <!-- result --> <body> <div id="app" data-v-app=""> <div id="rendered">Hello Vue!</div> </div> </body>
v-enter transition class has been renamed to v-enter-from and the v-leave transition class has been renamed to v-leave-from.
Vue 2
.v-enter, .v-leave-to { opacity: 0; } .v-leave, .v-enter-to { opacity: 1; }
Vue 3
.v-enter-from, .v-leave-to { opacity: 0; } .v-leave-from, .v-enter-to { opacity: 1; }
Using a <transition> as a component’s root will no longer trigger transitions when the component is toggled from the outside.
Vue 2
<!-- ChildComponent.vue --> <template> <transition> <div class="modal">Hello Vue!</div> </transition> </template> <!-- ParentComponent.vue --> <ChildComponent v-if="showModal" />
Vue 3
<!-- ChildComponent.vue - template --> <template> <transition> <div v-if="show" class="modal">Hello Vue!</div> </transition> </template> // ChildComponent.vue - script export default { props: ['show'] } <!-- ParentComponent.vue --> <ChildComponent :show"showModal" />
<transition-group> no longer renders a root element by default, but can still create one with the tag attribute.
Vue 2
<!-- span root element --> <transition-group> Hello Vue! </transition-group> <~-- ul root element --> <transition-group tag="ul"> <li v-for="item in items" :key="item"> {{ item }} </li> </transition-group>
Vue 3
<!-- span root element --> <transition-group tag="span"> Hello Vue! </transition-group> <~-- ul root element --> <transition-group tag="ul"> <li v-for="item in items" :key="item"> {{ item }} </li> </transition-group>
Prefix of component lifecycle events has been changed to vue:.
Vue 2
<ChildComponent @hook:updated="onUpdated">
Vue 3
<ChildComponent @vue:updated="onUpdated">
Lifecycle hooks beforeDestroy and destroyed have been renamed to beforeUnmount and unmounted respectively.
Vue 2
export default { beforeDestroy() { console.log('component instance is to be unmounted') }, destroyed() { console.log('component has been unmounted') } }
Vue 3
export default { beforeUnmount() { console.log('component instance is to be unmounted') }, unmounted() { console.log('component has been unmounted') } }
When watching an array, the callback will only trigger when the array is replaced. To trigger on mutation, the deep option must be specified.
Vue 2
export default { watch: { bookList(newVal, oldVal) { console.log('book list changed') } } }
Vue 3
export default { watch: { bookList: { handler(newVal, oldVal) { console.log('book list changed') }, deep: true } } }
<template> tags with no special directives (v-if, v-else-if, v-else, v-for, or v-slot) are now treated as plain elements and results in a native <template> element instead of rendering its inner content.
Vue 2
<div> <template> <div class="blue">Blue</div> </template> <template v-if="condition"> <div class="red">Red</div> </template> </div> <!-- result --> <div> <div class="blue">Blue</div> <div class="red">Red</div> </div>
Vue 3
<div> <template> <div class="blue">Blue</div> </template> <template v-if="condition"> <div class="red">Red</div> </template> </div> <!-- result --> <div> <template></template> <div class="red">Red</div> </div>