What's new in Vue 3.2?

Vue 3.2.0 is here!

Vue logo

Two months after v3.1.0, here is the v3.2.0 (and v3.2.1) release. Unlike the previous release which was focused on the “migration build”, (check out our previous blog post for more details) this release is focused on new features.

Let’s see what we have!

Performances improvements

A ton of small improvements have been made to improve Vue’s performances. The low-level virtual DOM functions generated by the compiler have been refactored and a lot of runtime checks are now skipped thanks to new hints from the compiler.

The reactivity part of the API has been improved as well, so the ref, watch, computed, effect functions are now faster! The nerdiest among you will be happy to learn that the reactivity now uses a class based implementation to improve the memory usage, and bitwise operations to make it faster 🚀

v-memo

The major performance improvement is the addition of the v-memo directive, which allows you to aggressively optimize templates in some edge cases. You can think of v-memo as an equivalent of shouldComponentUpdate in React, but available for elements or components in Vue.

Let’s say we have a template like this:

<div v-for="user of users" :key="user.id" v-memo="[user.name]">
  {{ user.name }}
</div>

Without v-memo, a change in the age of one of the users results in the creation of a new virtual div that will be compared by the virtual DOM algorithm to the previous one (check the chapter of our ebook about how Vue works under the hood if you want to learn more about this).

With v-memo, the virtual element is not recreated, and the previous one is re-used, except when the conditions of v-memo (here, the name of the user) change. This may look like a small improvement, but it’s actually a huge improvement in performance if you render a large list of elements. The popular (although debatable) js frameworks performance benchmark has a benchmark testing this particular use-case, and Vue v3.2 outperforms most frameworks with this new feature, without the need to create a dedicated component for the row and add tricks to improve the performances (like shouldComponentUpdate in React).

You can see that v-memo accepts an array of conditions, so you can write something like:

<div v-for="user of users" :key="user.id" v-memo="[user.name, selectedUserId === user.id]">
  <p :class="{ red: selectedUserId === user.id }">{{ user.name }}</p>
</div>

Then the div will be updated if either the user’s name changes or if the selected user check has a different result.

You can play with this online demo to get a feel of how it works.

.prop and .attr modifiers

Vue 3.2 brings back the .prop modifier from Vue 2, and adds the .attr modifier for v-bind. .prop was necessary in Vue 2 when you wanted to specify that a binding was to be applied to the underlying property and not to the attribute (which was the default).

Vue 3 has a different behavior, as it tries to bind to a property first, and then falls back to an attribute if there is no property with this name.

These modifiers are introduced for cases where you explicitly want to set a property or an attribute and not rely on the default behavior.

<a :title.prop="firstTabTooltip" :aria-selected.attr="isFirstTabSelected">First tab</a>

A shorthand version has also been introduced, with . for binding a property and ^ for binding an attribute.

<a .title="firstTabTooltip" ^aria-selected="isFirstTabSelected">First tab</a>

effectScope

Another addition to the core framework is the effectScope API.

The RFC #212 has been accepted and implemented in Vue 3.2. It adds a new function called effectScope that helps to collect several effects and to dispose of them easily.

You may know that watch, watchEffect, computed and others are bound to a particular component instance, and will be automatically destroyed by Vue when the component is destroyed. This makes sure that your application has no memory leak. But if you want to use these functions outside of the component, for example in a library you’re writing, you need to manually dispose of them, leading to code looking like that:

import { ref, computed, stop, watchEffect } from 'vue';

const quantity = ref(0);
const price = ref(10);
const total = computed(() => quantity.value * price.value);
const stopWatch = watchEffect(() => console.log(`total changed to ${total.value}`));

let effectsToStop = [];
effectsToStop.push(() => stop(total));
effectsToStop.push(stopWatch);
const stopAll = () => {
  effectsToStop.forEach(f => f())
  effectsToStop = []
};
// calling `stopAll()` disposes of all effects

You can try this online demo

effectScope() allows you to define a scope to run arbitrary code, and the effects run in this code are collected, and can be stopped on demand:

const quantity = ref(0);
const price = ref(10);
const scope = effectScope();
scope.run(() => {
  const total = computed(() => quantity.value * price.value);
  watchEffect(() => console.log(`total changed to ${total.value}`));
});
// calling `scope.stop()` disposes of all effects

You can have nested scope if needed, and the API also offers a global hook onScopeDispose that will be called when the scope is stopped.

effectScope is probably more useful if you are developing a library, but the RFC details some use-cases that might interest you.

Script setup is stable

<script setup> is no longer considered experimental! You can now use it in your applications without the fear of too much changes.

defineEmits and defineProps are now compiler macros, which means you don’t have to import them to use them (and you get a warning if you do). The previous function defineEmit has been removed and is replaced by definedEmits. <template inherit-attrs="false"> has been removed as well. useContext has been removed and replaced with useAttrs and useSlots. They can also be accessed with $attrs and $slots in the template.

<template>
  <h1>{{ greetings }}</h1>
</template>

<script setup>
  const props = defineProps({ mgs: String });
  const greetings = `Hello ${props.msg}`;
</script>

TS users will be happy to learn that this can be done in a type-safe manner, without relying on JS types:

<template>
  <h1>{{ greetings }}</h1>
</template>

<script setup lang="ts">
  const props = defineProps<{ msg: string }>();
  const greetings = `Hello ${props.msg}`;
</script>

The type can be an interface or a type alias, but there is currently a limitation as it needs to be declared in the same file, and can not be imported. If you want to provide default values, you can do so by using the withDefaults() helper. Note that this mechanism can be used with defineEmits as well.

Check out the online demo and the official RFC to learn more details.

Async setup

One of the new features of Vue 3 is the possibility to have asynchronous setup functions in components, and then display these components using Suspense. This is quite nice to use, but has a downside hard to see at first: whatever you call after the await call in your setup function will lose the context. For example:

async setup() {
  onMounted(() => console.log('hello'));
  const user = await fetchUser();
  onMounted(() => console.log('world')); // is never called
}

The lifecycle hooks that are called after the await call won’t be executed, as they need to be in the context of the component. They in fact throw a warning in the console if you try this code:

`onMounted` is called when there is no active 
component instance to be associated with. 
Lifecycle injection APIs can only be used during 
execution of setup(). If you are using async setup
(), make sure to register lifecycle hooks before 
the first await statement.

If you want to know why, I recommend you to read this discussion and this blog post from Anthony Fu which is a great explanation of the underlying issue.

If I’m mentioning this problem, it’s because Vue 3.2 takes care of it automatically if you use <script setup>, by wrapping the code with withAsyncContext. This helper restores the context after the await call, and makes this issue go away!

Expose API

Vue 3.2 adds a new API to define what is exposed by a component. Historically, everything that was usable in the template of a component was exposed to other components using it (via a template ref or when accessing $parent for example). Components can now more finely define what they want to expose to other components.

A new function, called expose can be used inside setup. For example, this Collapse component can expose only its toggle function, and not its collapsed variable.

export default defineComponent({
  setup(props, { expose }) {
    const collapsed = ref(true)
    const toggle = () => {
      collapsed.value = !collapsed.value;
    }
    // only expose `toggle` to the parent component
    expose({ toggle })
    return { collapsed, toggle }
  }
})

Note that all $ instance properties are automatically exposed, so a component using Collapse can access $props, $slots and others as well.

You can play with this example in this online playground

The same can be done when using <script setup> by calling the defineExpose() function.

New sugar ref experiment

The RFC #228 is evolving and a new version is implemented in Vue 3.2 (that I personally like better than the previous one). You can now write:

<template>
  <input type="number" v-model="count"> \* 5€
  <h1>{{ total }}</h1>
</template>

<script setup>
  let count = $ref(0)
  let total = $computed(() => count * 5)
</script>

$ref() avoids the need to use .value when updating the value of a ref. It’s a compiler macro, so there is no need to import it, and it feels less magical than the previous ref: syntax (detailed in our previous blog post).

This is still an experiment, so use with care, as it may change in the future. Note that this experiment needs to be explicitly enabled by setting refSugar: true in the compiler options (or via vue-loader v16.4).

The proposal also introduces other new syntactic sugar, with $computed(), $fromRefs() and $raw().

You can play with this online demo

Reactivity

New functions are available:

  • watchPostEffect as a convenience for using watchEffect with { flush: 'post' } options.
  • watchSyncEffect as a convenience for using watchEffect with { flush: 'sync' } options.

Custom Elements

Vue 3.2 now allows you to build custom elements using the defineCustomElement function.

const Hello = defineCustomElement({
  // this will be turned into Shadow DOM styles
  styles: [`div { color: red; }`],
  render() {
    return h('div', 'hello');
  }
});

It can even turn any Vue component into a custom element! It means you can use Vue components in a VanillaJS, Angular or React application if you want to. Vue 2 already offered a similar feature, but it required the use of a plugin and of the CLI. This is now builtin in Vue core:

import { defineCustomElement } from 'vue';
import User from `./User.vue`;
customElements.define('my-user', defineCustomElement(User));

You can then use <my-user></my-user> in your HTML.

Note that it supports slots, as they will render as native slots, but not scoped slots, as there is no similar feature in the Custom Element specification. Scoped CSS works thanks to the Shadow DOM, as you might expect.

The obvious downside of this approach is that the custom element still needs the Vue runtime, so it produces heavier elements than vanilla ones. Vue itself is quite small, so it might not be a big deal. It is also possible to bundle several custom elements without the runtime, and to only load Vue once on the page where you use them.

You’ll need vue-loader v16.4 to enable the custom element mode. By default, all components defined in a file named *.ce.vue will be loaded as custom elements, or you can set the option customElement to true to enable it for all components.

SFC playground

The SFC playground (used for most examples in this article) now supports TypeScript code for the script tags!

petite-vue

Evan You also worked on a new tool called petite-vue, which is an alternative to Vue thought for progressive enhancement. You just have to load the petite-vue script in your page, and you then sprinkle some directives around to turn it into a progressively-enhanced app. The template syntax is the same and the reactivity system is really similar.

<script src="https://unpkg.com/petite-vue" defer init></script>
<div v-scope="{ count: 0 }">
  {{ count }}
  <button @click="count++">inc</button>
</div>

This can be a nice alternative to Stimulus or AlpineJS frameworks.

That’s all for this release. Stay tuned for the next one!

All our materials (ebook, online training and training) are up-to-date with these changes if you want to learn more!



blog comments powered by Disqus