Vue 3
New features, Breaking changes & a Migration path
~/ whoami
Vue.js Enthusiast
Proud VuejsAthens co-organizer
Currently working for Glovo Barcelona
fadamakis
What to expect
-
Notable Breaking Changes
-
New Features
-
Composition API
-
Supporting Libraries
-
Migration Path
-
Additional Resources
Global API
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.
Global API
Global vs Instance API
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 |
Fragments
<!-- 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
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>
Teleport
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
}
}
})
key attribute
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.
Key attribute (v-for)
<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.
Key attribute (v-if)
v-if vs v-for Precedence
If used on the same element, v-if will have higher precedence than v-for
<template v-if="list.length" v-for="item in list" :key="item.id">
<div>...</div>
</template>
Functional Components
In Vue 2, functional components had two primary use cases:
- to return multiple root nodes
- as a performance optimization, because they initialized much faster than stateful components
- to create dynamic components
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>
Functional Components
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>
Functional Components
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
Async Components
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.
Async Components
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
Scoped Slots
this.$scopedSlots removed and they are now unified into the $slots option.
$attrs changes
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
$attrs changes
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>
Component lifecycle hooks
<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>
Custom Directives
Vue 2.x
Vue 3.x
The hook functions for directives have been renamed to better align with the component lifecycle.
- bind - Occurs once the directive is bound to the element. Occurs only once.
- inserted - Occurs once the element is inserted into the parent DOM.
- update - This hook is called when the element updates, but children haven't been updated yet.
- componentUpdated - This hook is called once the component and the children have been updated.
- unbind - This hook is called once the directive is removed. Also called only once.
- created - new! This is called before the element's attributes or event listeners are applied.
- bind → beforeMount
inserted → mounted - beforeUpdate: new! This is called before the element itself is updated, much like the component lifecycle hooks.
- update → removed! There were too many similarities to updated, so this is redundant. Please use updated instead.
- componentUpdated → updated
beforeUnmount: new! Similar to component lifecycle hooks, this will be called right before an element is unmounted. - unbind → unmounted
Custom Directives
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
Transition Class Change
.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
Events API
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.
Filters
<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.
Composition API
Composition API
// 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 () { ... }
}
}
Composition API
Composition API
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
}
}
}
Composition API
// 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
Composition API
// 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
Composition API
// 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
Composition API
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
Composition API
- Less & Cleaner code
- Flexibility
- Built with functions
- Better typescript support
- Tooling friendly
- Learning curve
- Two ways of doing the same thing
Supporting Libraries
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.
Supporting Libraries
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.
Browser Compatibility
IE11 dropped from the main bundle.
Need to add an extra polyfill for proxies in order to support IE
Migration guide
- Learn about new features and breaking changes
- Replace deprecated features when possible (Event bus, filters)
- Upgrade to Vue 2.7 *
- Refactor every breaking change to work the Vue 3 way
- Upgrade to Vue 3 🥳🥳🥳
* 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.
Additional resources
Conclusion
Vue 3
By fadamakis
Vue 3
Vue 3- New features, Breaking changes & a Migration path
- 459