Migration to Vue 3, Part 7. – Other Minor Changes

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>