March 4, 2021
Vue.js is a growing open-source front-end framework for building single-page applications and user interfaces. Since its first release, Vue.js has been increasing its popularity and gaining new users thanks to its developer-friendly syntax, ease of use, and famously helpful documentation.
In September 2020, the third version of Vue.js was released, and in February 2022, it became the new default version. Vue 3 is smaller, easier to maintain, and has more handy features.
Here are some of the new features in Vue 3 which grabbed my attention.
One of the Vue.js concepts allows us to create large applications built out of smaller, reusable components. This allows us to easily manage the development process.
In Vue 2 we had three ways to reuse code:
Mixin factories are functions that return a customized version of mixin and give us the possibility to modify such mixin by sending a configuration to it.
They are special types of slots that work as a reusable template where we can pass data, instead of already-rendered elements.
As you see, we have plenty of options, but each of them has its limitations. Also in Vue 2, we could write components by organizing them only by components options: components, props, data, computed, methods, and lifecycle methods, so during this process, our stand-alone components could get bigger and bigger, and as a result, become less readable and hardly maintainable for future development.
Vue 3 offers us a feature that allows us to organize components by logical concerns — this feature is called Composition API. It’s an optional feature that allows us to write components in another way using more advanced syntax, you have to remember that you can still use the regular one.
Composition API gives the opportunity to reuse code between components.
Vue 2 organizing code with default Options API and reuse code with mixin:
<template>
<div id="app">
<div class="content">
<h1>{ {title} }</h1>
<label for="title">
<input type="text" name="title" v-model="inputVal" />
</label>
<button @click="addTitle" class="add-btn">Add</button>
<button @click="clearInput">Clear</button>
<button @click="pressMe">Clear</button>
</div>
</div>
</template>
<script>
const exampleMixin = {
data() {
return {
someInfo: "This is our mixin!"
}
},
methods: {
pressMe() {
alert(this.someInfo);
}
}
}
export default {
mixins: [exampleMixin],
data() {
return {
inputVal: "",
title: ""
};
},
methods: {
clearInput() {
this.inputVal = "";
},
addTitle() {
this.title = this.inputVal;
}
}
};
</script>
As you can see, in the regular syntax our code is organized by component options (data & methods).
Vue 3 organizing code with Composition API:
<template>
<div id="app">
<div class="content">
<h1>{ {title} }</h1>
<label for="title">
<input type="text" name="title" v-model="inputVal" />
</label>
<button @click="addTitle" class="add-btn">Add</button>
<button @click="clearInput">Clear</button>
</div>
</div>
</template>
<script>
import { ref } from "vue";
export default {
setup() {
const inputVal = ref("");
const title = ref("");
function clearInput() {
this.inputVal = "";
}
function addTitle() {
this.title = this.inputVal;
}
return { inputVal, title, clearInput, addTitle };
}
};
</script>
The Composition API is another way that Vue 3 gives us to reuse code. This feature has advantages and limitations.
Earlier called Portal, Teleport allows us to decide under which parent element we can place a piece of HTML code to render. The main advantage of Teleport is that you can place elements anywhere in the DOM tree, so we don’t need to nest components or in some cases split the code into two components.
It accepts a property called “to” in which we can specify where we want to put our elements, the one condition being that the target element should exist before the component has mounted.
So in Vue 3, this should look like this:
<teleport to="#app">
<div>Foo</div>
</teleport>
<teleport to="#app">
<div>Bar</div>
</teleport>
will compile to:
<div id="app">
<div>Foo</div>
<div>Bar</div>
</div>
You can send elements from multiple sources. Also, we need to remember that the imported elements do not inherit styles.
Teleport is very useful when we work with elements that need to be set in a specific order in the DOM, for instance: modal windows, some kind of alerts, or dialogs.
defineAsyncComponent
function:
const asyncComponent = () => import ('./AsyncComponent.vue')
const asyncComponent = defineAsyncComponent(() => import ('./AsyncComponent.vue'))
In Vue 2, we cannot have multiple root elements, this was one of the limitations in the second version and often emit warnings if we tried to create this. Now in Vue 3, we don’t need to wrap root elements in one main element — we can have multiple nodes in the root.
<template>
<div>
<header></header>
<section></section>
</div>
</template>
<template>
<header></header>
<section></section>
</template>
Filters are no longer supported, instead, we can use computed properties or methods.
<template>
<div id="app">
<span>{ { amount | usd } }</span>
</div>
</template>
<script>
export default {
data() {
return {
amount: 10
}
},
filters: {
usd(val) {
return val + '$'
}
}
}
</script>
<template>
<div id="app">
<span>{ { usd } }</span>
</div>
<template>
<script>
export default {
data() {
return {
amount: 10
}
},
computed: {
usd() {
return this.amount + '$'
}
}
}
</script>
Vue 3 allows us to perform lazy loading components with the Suspense component.
The <Suspense>
component allows us to provide some fallback content while our user is waiting for the data, so we can see something else for instance spinner or some text.
<Suspense>
<template #default>
<!-- component/component which makes an asynchronous call -->
</template>
<template #fallback>
<!-- Content to display when loading -->
Loading...
</template>
</Suspense>
CSS variables allow us to store a value in one place and then reuse it elsewhere in our CSS code — this is a nice way to have cleaner code.
In Vue 3, a single file component supports v-bind() function in <style>
tag, so we can bind a value from the component state to any CSS property. This allows us to dynamically change the value of some CSS properties.
<template>
<div id="app">
<div class="content">
<h1>Hello world!</h1>
<button @click="modalToggle">Click me</button>
</div>
<div v-if="showModal" class="modal">
<h2>Modal window</h2>
<button class="close-btn" @click="modalToggle">Close the window</button>
</div>
<div class="shadow"></div>
</div>
</template>
<script>
export default {
data() {
return {
displayShadow: "none",
showModal: false
};
},
methods: {
modalToggle() {
this.showModal = !this.showModal;
this.displayShadow = this.showModal ? "block" : "none";
}
}
};
</script>
<style>
.shadow {
display: v-bind(displayShadow);
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgb(46 46 46 / 31%);
z-index: 10;
opacity: 1;
}
</style>
In the code above, you can see a simple example of using such a feature. We wanted to show and hide modal window shadow depending on modal is visible or not. So we bound the displayShadow
value to the display
property of the .shadow
class to conditionally change its value in the modalToggle method which is responsible for opening and closing of the modal window.
<style scoped>
changesIf we use scoped styles in SFC, it means that the CSS applies only to this current component. This feature can help developers provide more consistent custom CSS in single file component scoped styles.
<style>
tag>>>
and /deep/
combinators which are deprecated now we can use ::v-deep()
or its shorter version :deep()
.::v-slotted()
/:slotted()
pseudo-element. So in that way child scoped styles do not affect the slot component.In Vue.js we’re using v-model for two-way binding. The most popular way to use it is of course the form elements. In Vue 2, we can use only one v-model in a single component, but Vue 3 allows us to pass multiple v-models to our components by specifying their names. Let’s see how it’ll look.
<template>
<div id="app">
<exampleForm v-model="person" />
</div>
</template>
<script>
export default {
data() {
return {
person: {
age: 20,
name: "Jan"
}
};
}
};
</script>
<template>
<div id="app">
<exampleForm v-model:age="person.age" v-model:name="person.name" />
</div>
</template>
<script>
export default {
data() {
return {
person: {
age: 20,
name: "Jan"
}
};
}
};
</script>
The destroyed
lifecycle was renamed to unmounted
. The beforeDestroy
was changed to beforeUnmount
.
Options API (default) |
Composition API (hook inside setup) |
beforeCreate |
Not needed |
created |
Not needed |
beforeMount |
onBeforeMount |
mounted |
onMounted |
beforeUpdate |
onBeforeUpdate |
updated |
onUpdated |
beforeUnmount |
onBeforeUnmount |
unmounted |
onUnmounted |
errorCaptured |
onErrorCaptured |
renderTracked |
onRenderTracked |
renderTriggered |
onRenderTriggered |
<template>
render changes:In the new version <template>
tag will render its content only if it will have a special directive in it, other than that, it will be treated as a plain HTML element and will render into plain <template>
tag.
Download our comprehensive guide with all the insights, data, and case studies about Vue.js.
I’ve presented only a small piece of all the changes that Vue offers us in its new version. To learn more, I encourage you to take a look at the Official Vue Docs and of course our newest Vue.js report.