What's new in Vue 3.2?
Vue 3.2.0 is here!
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 usingwatchEffect
with{ flush: 'post' }
options.watchSyncEffect
as a convenience for usingwatchEffect
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!