Exciting new features in Vue 3

Written by Filip Rakowski

In the previous article we learned about the performance improvements that Vue 3 will bring. We already know that apps written in the new major version of Vue will perform very well but performance isn’t the most important part. The thing that matters most to us developers, is how the new release will affect the way we write our code.

As you could expect, Vue 3 brings a lot of new exciting features. Thankfully Vue team mostly introduced additions and improvements over current APIs rather than major changes so people that already know Vue 2 should quickly feel comfortable with new syntaxes.

Let's start with the API that most of you probably heard about...

Composition API

Composition API is most commonly discussed and featured syntax of the next major version of Vue. It’s a completely new approach to logic reuse and code organization.

Currently we build components with what we call the Options API. To add logic into Vue component we fill (option) properties such as data, methods, computed etc. The biggest downside of this approach is the fact that this is not a working JavaScript code per se. You needed to know exactly which properties are accessible in the template as well as the behavior of this keyword. Under the hood, the Vue compiler needs to transform this properties to working code. Because of that we can’t benefit from things like autosuggestions or type checking.

Composition API aims to solve this issue by exposing mechanisms currently available through component properties as JavaScript functions. Vue core team describes Composition API as “a set of additive, function-based APIs that allow flexible composition of component logic“. Code written with Composition API is more readable and there is no magic behind the scenes which makes it easier to read and learn.

Let’s see a very simple example of a component that uses new Composition API to understand how it works.

<template>
  <button @click="increment">
    Count is: {{ count }}, double is {{ double }}, click to increment.
  </button>
</template>

<script>
import { ref, computed, onMounted } from 'vue'

export default {
  setup() {
    const count = ref(0)
    const double = computed(() => count.value * 2)

    function increment() {
      count.value++
    }

    onMounted(() => console.log('component mounted!'))

    return {
      count,
      double,
      increment
    }
  }
}
</script>

Now let’s break this code down into pieces to understand what happened

import { ref, computed, onMounted } from 'vue'

As I mentioned before Composition API is exposing component properties as functions so the first step is to import the functions that we need. In our case we need to create reactive reference with ref, computed property with computed and access mounted lifecycle hook with onMounted.

Now you probably wonder what is this mysterious setup method?

export default {
  setup() {

In short words it’s just a function that returns properties and functions to the template. That’s it. We are declaring all reactive properties, computed properties, watchers and lifecycle hooks here and then return them so they can be used in a template.

What we don’t return from the setup function will not be available in the template.

const count = ref(0)

According to above we are declaring reactive property called count with ref function. It can wrap any primitive or object and return it’s reactive reference. The value of passed element will be kept in a value property of the created reference. For example if you want to access value of count reference you need to explicitly ask for count.value.

const double = computed(() => count.value * 2)

function increment() {
  count.value++
}

…and this is exactly what we did when declaring computed property double as well as in increment function.

onMounted(() => console.log('component mounted!'))

With onMounted hook we are logging some message when component is mounted just to show you that you can 😉

return {
  count,
  double,
  increment
}

At the end we are returning count and double properties with increment method to make them available in a template.

<template>
  <button @click="increment">
    Count is: {{ count }}, double is {{ double }}. Click to increment.
  </button>
</template>

And voila! Now we can access the properties and functions returned by setup method in the template the same way as they were declared through the old Options API.

This is a simple example, that would be easy to achieve with the Options API as well. The true benefit of the new Composition API is not just to code in a different way, the benefits reveal themselves when it comes to reusing our code/logic.

Code reuse with Composition API

There are more advantages of the new Composition API. Think about code reuse. Currently if we want to share some code across other components there are two options available - mixins and scoped slots. Both have their drawbacks.

Let’s say that we want to extract counter functionality and reuse it in other components. Below you can see how it could be used with available APIs and new Composition API:

Let’s start with mixins:

import CounterMixin from './mixins/counter'

export default {
  mixins: [CounterMixin]
}

The biggest downside of mixins is the fact that we know nothing about what it’s actually adding to our component. It makes it not only hard to reason about but can also lead to name collisions with existing properties and functions.

It’s time for scoped slots.

<template>
  <Counter v-slot="{ count, increment }">
     {{ count }}
    <button @click="increment">Increment</button> 
  </Counter> 
</template>

With scoped slots we know exactly what properties we can access through v-slot property so it’s much easier to understand the code. The downside of this approach is that we can access it only in a template and it’s available only in Counter component scope.

Now it’s time for Composition API:

function useCounter() {
  const count = ref(0)
  function increment () { count.value++ }

  return {
    count,
    incrememt
  }
}

export default {
  setup () {
    const { count, increment } = useCounter()
    return {
      count,
      increment
    }
  }
}

Much more elegant isn’t it? We are not limited by neither template nor components scope and know exactly what properties from counter we can access. In addition we can benefit from code completion available in our editor because useCounter is just a function that returns some properties. There is no magic behind the scenes so editor can help us with type checking and suggestions.

It’s a more elegant way of using third party libraries as well. For example if we want to use Vuex we can explicitly use useStore function instead of polluting Vue prototype (this.$store). This approach erases behind the scenes magic from Vue plugins as well.

const { commit, dispatch } = useStore()

If you want to learn more about Composition API and it’s use cases I highly recommend reading this document from Vue team that explains reasoning behind new API and suggests it’s best use cases. There is also great repository with examples of Composition API usage by Thorsten Lünborg from Vue core team.

Global mounting/configuration API change

We can find another major change in the way we are instantiating and configuring our application. Let’s see how it works right now:

import Vue from 'vue'
import App from './App.vue'

Vue.config.ignoredElements = [/^app-/]
Vue.use(/* ... */)
Vue.mixin(/* ... */)
Vue.component(/* ... */)
Vue.directive(/* ... */)

new Vue({
  render: h => h(App)
}).$mount('#app')

Currently we are using global Vue object to provide any configuration and create new Vue instances. Any change made to Vue object will affect every Vue instance and component.

Now let’s see how it will work in Vue 3:

import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

app.config.ignoredElements = [/^app-/]
app.use(/* ... */)
app.mixin(/* ... */)
app.component(/* ... */)
app.directive(/* ... */)

app.mount('#app')
Vue School Prices Will Increase Piggy Bank illustration

As you probably noticed now every configuration is scoped to a certain Vue application defined with createApp.

It could make your code easier to understand and less prone to unexpected issues caused by third party addons. Currently if some third party solution is modifying Vue object it can affect your application in unexpected ways (especially with global mixins) which would not be possible with Vue 3.

This API change is currently discussed in this RFC which means it could potentially change in the future.

Fragments

Another exciting addition that we can expect in Vue 3 are Fragments.

What are fragments you may ask? Well if you create a Vue component it can only have one root node.

It means that component like this cannot be created:

<template>
  <div>Hello</div>
  <div>World</div>
</template>

The reason of that is Vue instance that represents any Vue component needs to be binded into a single DOM element. The only way you can create a component with multiple DOM nodes is by creating a functional component that doesn’t have underlying Vue instance.

It turns out React community had the same problem. The solution they came up with was a virtual element called Fragment. It looks more or less like this;

class Columns extends React.Component {
  render() {
    return (
      <React.Fragment>
        <td>Hello</td>
        <td>World</td>
      </React.Fragment>
    );
  }
}

Even though Fragment looks like a normal DOM element it’s virtual and won’t be rendered in a DOM tree at all. This way we can bind component functionality into a single element without creating a redundant DOM node.

Currently you can use Fragments in Vue 2 with vue-fragments library and in Vue 3 you will have it out of the box!

Suspense

Another great idea from React ecosystem that will be adopted in Vue 3 is Suspense component.

Suspense suspends your component rendering and renders a fallback component until a condition is met. During Vue London Evan You briefly touched this topic and showed the API we can more or less expect. As it turns out, Suspense will just be a component with slots:

<Suspense>
  <template >
    <Suspended-component />
  </template>
  <template #fallback>
    Loading...
  </template>
</Suspense>

The fallback content will be shown until Suspended-component will fully render. Suspense can either wait till component will be downloaded if that’s an async component or perform some async actions in setup function.

Multiple v-models

V-model is a directive we can use to achieve two-way binding on a given component. We can pass a reactive property and modify it from the inside of a component.

We know v-model well from the form elements:

<input v-bind="property />

But did you know you can use v-model with every component? Under the hood v-model is just a shortcut for passing value property and listening to input event. Rewriting above example to below syntax will have exactly same effect:

<input 
  v-bind:value="property"
  v-on:input="property = $event.target.value"
/>

We can even change names of default property and event with components model property:

model: {
  prop: 'checked',
  event: 'change'
}

As you can see v-model directive can be a really helpful syntactic suger if we want to have a two-way binding in our components. Unfortunately you can only have one v-model per component.

Fortunately this won’t be a problem in Vue 3! You will be able to give v-model properties names and have have as many of them as you want. Below you can find example of two v-model's in a form component:

<InviteeForm
  v-model:name="inviteeName"
  v-model:email="inviteeEmail"
/>

This API change is currently discussed in this RFC which means it could potentially change in the future.

Portals

Portals are special components meant to render certain content outside the current component. It’s also one of the features natively implemented in React. This is what React documentation says about portals:

Portals provide a first-class way to render children into a DOM node that exists outside the DOM hierarchy of the parent component."

It’s a very nice way of dealing with modals, popups and generally components that are meant to appear on top of the page. By using portals you can be sure that none of the host component CSS rules will affect the component that you want to display and reliefs you from doing nasty hacks with z-index.

For every portal we need to specify it’s target destination where portal content will be rendered. Below you can see the implementation from portal-vue library which adds this functionality to Vue 2:

<portal to="destination">
  <p>This slot content will be rendered wherever thportal-target with name 'destination'
    is  located.</p>
</portal>

<portal-target name="destination">
  <!--
  This component can be located anywhere in your App.
  The slot content of the above portal component wilbe rendered here.
  -->
</portal-target>

Vue 3 will ship with out of the box support for portals!

New custom directives API

Custom directives API will slightly change in Vue 3 just to better align with components lifecycle. This change should make the API easier to understand and learn for newcomers as it’s now more intuitive.

This is a current custom directives API:

const MyDirective = {
  bind(el, binding, vnode, prevVnode) {},
  inserted() {},
  update() {},
  componentUpdated() {},
  unbind() {}
}

…and this is how it will look like in Vue 3.

const MyDirective = {
  beforeMount(el, binding, vnode, prevVnode) {},
  mounted() {},
  beforeUpdate() {},
  updated() {},
  beforeUnmount() {}, // new
  unmounted() {}
}

Even though it’s a breaking change it should be easily covered with a Vue compatibility build.

This API change is currently discussed in this RFC which means it could potentially change in the future.

Psst! You can learn how to master custom directives in our course.

Summary

Apart from Composition API that is the biggest major new API in Vue 3 we can find a lot of smaller improvements as well. We can see that Vue is moving towards better developer experience and simpler, more intuitive APIs. It’s also great to see that Vue team decided to adopt many ideas that are currently available only through third party libraries to the core of the framework.

The list above conatins only major API changes and improvements. If you’re curious about other ones be sure to check Vue RFCs repository..

Vue School Prices Will Increase Piggy Bank illustration

Leave a Reply

Your email address will not be published. Required fields are marked *

Up Next:

Faster Web Applications with Vue 3

Faster Web Applications with Vue 3