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>