What's new in Vue 3.5?
Vue 3.5.0 is here!
The last minor release was v3.4.0 in December. Since then, we have seen quite a few patch releases, and some interesting new features.
Let’s see what we have in this release!
Props destructuration
Props destructuration was introduced as an experiment in Vue 3.3 (as part of the reactive transform experiment) and is now stable in Vue 3.5.
So instead of:
const props = withDefaults(defineProps<{ name?: string }>(), { name: "Hello" });
watchEffect(() => console.log(props.name));
You can now write:
const { name = "Hello" } = defineProps<{ name?: string }>();
watchEffect(() => console.log(name));
// ☝️ This gets compiled to the same code as the previous example
// so we don't lose the reactivity of the props
You no longer need the propsDestructure: true
flag in the compiler options to use this feature,
so you can remove it if you have it.
You can however disable this feature by setting propsDestructure: false
in the compiler options,
or even throw an error if you want to enforce the use of the previous syntax by setting propsDestructure: 'error'
.
Read more about this feature in the RFC.
useTemplateRef
As you probably know, Vue lets you grab a reference to an element in a template, using the ref="key"
syntax. The framework then populates a Ref named key
in the setup of the component.
For example, to initialize a chart, you usually write code looking that:
<canvas ref="chart"></canvas>
// 👇 special ref that Vue populates with the element in the template
const chart = ref<HTMLCanvasElement | null>(null);
onMounted(() => new Chart(chart.value!, /* chart options */));
This API felt a bit awkward, as nothing was pointing out that this ref was “special” at first glance. It also forced developers to pass this ref around to composables.
For example, if you wanted to build a useChart
composable, you had to write it like this:
export function useChart(chartRef: Ref<HTMLCanvasElement | null>) {
onMounted(() => new Chart(chartRef.value!, /* chart options */));
}
And then call it in your component by passing it the ref:
const chart = ref<HTMLCanvasElement | null>(null);
useChart(chart);
Vue v3.5 introduces a new composable called useTemplateRef
to grab a reference in the template:
// useTemplateRef expects the key of the element in the template
const chartRef = useTemplateRef<HTMLCanvasElement>('chart');
onMounted(() => new Chart(chartRef.value!, /* chart options */));
The type of chartRef
is a read-only ShallowRef<HTMLCanvasElement | null>
.
In addition to a more explicit name and usage, the new function is usable directly inside a composable. This simplifies the pattern we saw above, as useChart
can simply be written:
export function useChart(chartKey: string) {
const chartRef = useTemplateRef<HTMLCanvasElement>(key);
onMounted(() => new Chart(chartRef.value!, /* chart options */));
}
and then used in a component:
useChart('chart');
This is a nice improvement!
useId
A new composition function called useId
has been added to generate a unique ID.
This feature is probably already familiar to React developers or Nuxt developers who use the useId
composable.
This can be useful when you need to generate an ID for an HTML element, for example when you use a label with an input, or for accessibility attributes:
<label :for="id">Name</label>
<input :id="id" />
As the component can be rendered many times, you need to ensure that the ID is unique.
This is where useId
comes in:
const id = useId();
useId
guarantees that the generated ID is unique within the application.
By default, Vue generates an ID with a prefix of v-
followed by a unique number
(that increments when new components are rendered).
The prefix can be customized by using app.config.idPrefix
.
useId
also guarantees that the ID is stable between server-side rendering and client-side rendering, to avoid mismatching errors.
Lazy hydration strategies
Asynchronous components, defined with defineAsyncComponent
,
can now control when they should be hydrated using a new hydrate
option.
Vue provides four strategies for hydration in v3.5:
hydrateOnIdle()
: the component will be hydrated when the browser is idle (you can specify a timeout if needed, asrequestIdleCallback
, which is used internally, allows).hydrateOnVisible()
: the component will be hydrated when it becomes visible in the viewport (implemented using anIntersectionObserver
). Additional options supported by theIntersectionObserver
API can be passed to the strategy, likerootMargin
to define the margin around the viewport. So you can usehydrateOnVisible({ rootMargin: '100px' })
to hydrate the component when it is 100px away from the viewport.hydrateOnInteraction(event)
: the component will be hydrated when the user interacts with the component with a defined event, for examplehydrateOnInteraction('click')
. You can also specify an array of events.hydrateOnMediaQuery(query)
: the component will be hydrated when the media query matches. For example,hydrateOnMediaQuery('(min-width: 600px)')
will hydrate the component when the viewport is at least 600px wide.
You can also define a define custom strategy if you want to.
Here is an example of how to use these strategies:
import { defineAsyncComponent, hydrateOnVisible } from 'vue'
const User = defineAsyncComponent({
loader: () => import('./UserComponent.vue'),
hydrate: hydrateOnVisible('100px')
});
data-allow-mismatch
Vue 3.5 now supports a new attribute called data-allow-mismatch
,
that can be added to any element to allow client/server mismatch warnings
to be silenced for that element.
For example, if you have a component that renders the current date like this:
<div>{{ currentDate }}</div>
you might get a warning if the server and the client render the date at different times:
[Vue warn]: Hydration text content mismatch on <div>
- rendered on server: Jul 26, 2024
- expected on client: Jul 27, 2024
You can silence this warning by adding the data-allow-mismatch
attribute:
<div data-allow-mismatch="text">{{ currentDate }}</div>
The value of the attribute can be:
text
to silence the warning for text contentchildren
to silence the warning for children contentclass
to silence the warning for class mismatchstyle
to silence the warning for style mismatchattribute
to silence the warning for attribute mismatch
Better types
A few improvements have been made to help the tooling understand the Vue API better.
For example, components that use expose
will now have a more correct type.
An effort has also been made for directives. It is now possible to specify the allowed modifiers for a directive in the type definition:
// can be used as v-focus.seconds in the template
export const vFocus: Directive<
HTMLInputElement,
boolean,
'seconds' /* 👈 New! only 'seconds' is allowed as modifier */
> = (el, binding) => {
const secondsModifier = binding.modifiers.seconds; // autocompletion works here
...
}
The built-in directives have also been improved to leverage this new feature.
Another improvement concerns the computed
function:
you can now define a getter and a setter with different types (it was already working but TS was complaining):
const user = ref<UserModel>({ name: 'Cédric' });
const json = computed({
get: () => JSON.stringify(user.value),
// 👇 the setter receives a UserModel instead of a string
set: (newUser: UserModel) => user.value = newUser;
}); // typed as ComputedRef<string, UserModel>
console.log(json.value); // 👈 a string
json.value = { name: 'JB' }; // 👈 no error
app.onUnmount()
It is now possible to register a callback that will be called when the app is unmounted
(i.e when the app.unmount()
method is called)
This can be useful to clean up resources or to log something if you unmount your application,
but it is even more useful for plugin developers.
Here is an example:
const myPlugin: Plugin = {
install (app: App) {
function cleanupSomeSideEffect() { /* ...*/ }
// Register the cleanup function to be called when the app is unmounted
app.onUnmount(cleanupSomeSideEffect)
}
Watcher novelties
deep watch
The watch
function had a deep
option since the beginning.
It allows watching deeply nested properties of a ref:
const obj = ref({ super: { nested: { prop: 1 } } });
watch(obj, () => {
// called when the ref or one of its nested properties changes
console.log('nested prop changed');
}, { deep: true });
You don’t need it for a reactive
object though,
as watch
will automatically watch deeply nested properties of a reactive object.
In that case, deep
can be set to false
if you want to disable this behavior.
The novelty introduced in Vue 3.5 is that you can now use deep
with a specific depth:
const obj = ref({ super: { nested: { prop: 1 } } });
watch(obj, () => {
// called when the ref or the first level of its nested properties changes
console.log('nested prop changed');
}, { deep: 1 }); // 👈 deep can now be a number
pause/resume
The watch
and watchEffect
can now be paused and resumed, in addition to being stopped.
Until now, you could only stop a watcher, which would prevent it from being called again:
const stop = watch(obj, () => {
console.log('obj changed');
});
stop(); // 👈 stop the watcher
Now, you can pause and resume a watcher:
const { pause, resume, stop } = watch(obj, () => {
console.log('obj changed');
});
pause(); // 👈 pause the watcher
resume(); // 👈 resume the watcher
stop(); // 👈 stop the watcher
onWatcherCleanup
A new API called onWatcherCleanup
has been added to register a callback that will be called when a watch
/watchEffect
is cleaned up.
This is similar to what the onCleanup
parameter of watchers does,
but it allows to use the cleanup function in functions called inside a watcher.
Before
// starts an interval, called in the watchEffect below
function startInterval(intervalTime, onCleanup) {
const id = window.setInterval(() => console.log('hello'), intervalTime)
// we use onCleanup here to clear the interval
onCleanup(() => window.clearInterval(id));
}
const intervalTime = ref(1000);
watchEffect((onCleanup) => {
console.log('Interval time changed', intervalTime.value);
// we need to pass onCleanup to startInterval
startInterval(intervalTime.value, onCleanup);
});
Now
function startInterval(intervalTime) {
const id = window.setInterval(() => console.log('hello'), intervalTime)
//👇 we can now use onWatcherCleanup
onWatcherCleanup(() => window.clearInterval(id));
}
const intervalTime = ref(1000);
watchEffect(() => {
console.log('Interval time changed', intervalTime.value);
//👇 no need to pass onCleanup anymore
startInterval(intervalTime.value);
});
onWatcherCleanup
throws a warning if there is no current active effect.
This warning can be silenced by passing a second parameter to onWatcherCleanup
:
onWatcherCleanup(() => {
// cleanup code
}, true /* 👈 no warning */);
A similar API has been introduced for the low level effect
function, called onEffectCleanup
.
Trusted types
Vue 3.5 now supports Trusted Types.
It should work out of the box by default.
This is done by automatically converting the strings generated by the compiler into TrustedHTML
when they are used in a context where a Trusted Type is expected.
v-html
is not supported out-of-the-box, but can also be used if you declare a custom policy.
throwUnhandledErrorInProduction
A new option has been added to the app configuration to throw unhandled errors in production. This can be useful to catch errors that are currently not caught because the default behavior is to log them in the console.
const app = createApp(App);
app.config.throwUnhandledErrorInProduction = true;
With this option enabled, you’ll easily catch errors when rendering your application in production.
Note that the default is false
to avoid breaking existing applications.
Teleport
deferred Teleport
It is now possible to add a defer
attribute to Teleport
to mark the component as deferred.
When doing so, the target of the teleportation doesn’t have to already exist:
even if it appears later, the target can still be resolved.
A deferred Teleport waits until all other DOM content in the same update cycle has been rendered before locating the target container.
So we can now use Teleport
with targets located in other components (as long as they are mounted in the same tick), or even use Teleport
and a target inside a Suspense
(whereas you previously had to target a container outside of the Suspense
component).
<Suspense>
<Teleport defer to="#target">...</Teleport>
<div id="target"></div>
</Suspense>
Teleport and Transition
It is now possible to use a Teleport
component directly inside a Transition
component,
thus allowing to animate the appearance and the disappearance of an element in a different place in the DOM. This used to throw an error in Vue 3.4.
You can check out this playground from the PR author edison1105, showcasing this new feature.
Custom elements
Vue v3.5 adds a bunch of features for custom elements. As I don’t personally use them, I’ll just list the most notable here:
defineCustomElement
now supports disabling ShadowDom by setting theshadowRoot
option tofalse
;- a
useHost()
composable has been added to get the host element and auseShadowRoot()
composable has been added to get the shadow root of the custom element (which can be useful for CSS in JS); emit
now supports specifying event options, likeemit('event', { bubbles: true })
;expose
is now available in custom elements;- custom elements can now define a
configureApp
method to configure the associated app instance, for example to use plugins like the router;
Developer experience
The Vue compiler will now emit a warning if you write invalid HTML nesting in your template (for example, a div
inside a p
):
warning: <div> cannot be child of <p>, according to HTML specifications. This can cause hydration errors or potentially disrupt future functionality.
We also have a new warning when a computed
is self-triggering (i.e. it writes to one of its dependencies in its getter):
Computed is still dirty after getter evaluation
likely because a computed is mutating its own dependency in its getter.
State mutations in computed getters should be avoided.
Check the docs for more details: https://vuejs.org/guide/essentials/computed.html#getters-should-be-side-effect-free
This warning is not enabled by default, you need to set app.config.warnRecursiveComputed
to true
in your application configuration.
Performances
A whole lot of optimizations have been made around reactivity. The first notable one is that the reactivity system now uses a new algorithm to track dependencies. It now relies on version counting and doubly linked lists. If I’m not mistaken, this is really similar to what Preact did and explained in this really interesting article. This brings a few performance improvements (most notably around memory usage) and should make the reactivity system more predictable (computed values should now never be stale).
The other notable change is that array manipulation should also be faster, especially for large arrays.
News from the ecosystem
Vapor mode
Vapor (@vue/vapor
) is making progress within its repository. You can play with it using the online REPL here.
Vue router
Vue Router 4.4.0 is out and offers the possibility to have typed routes!
This can be done manually by adding the types yourself to your project
(for example in env.d.ts
):
interface RouteNamedMap {
// a route with no params, named 'users' and matching '/users'
users: RouteRecordInfo<"users", "/users", Record<never, never>>;
// a route with a param named 'id', named 'user' and matching '/user/:id'
user: RouteRecordInfo<
"user",
"/users/:id",
{ id: string | number }, // raw parameters (allows to use the route with a number or a string)
{ id: string } // parameters we get with useRoute
>;
}
declare module "vue-router" {
interface TypesConfig {
RouteNamedMap: RouteNamedMap;
}
}
Then when you use a RouterLink
or router.push
, you get a nice auto-completion
and type-checking:
<!-- 👇 you get an error if the route name has a typo -->
<!-- or if you define a parameter that does not exist or forget to define it -->
<RouterLink :to="{ name: 'users', params: { id: user.id } }">User</RouterLink>
Then when using useRoute
, you get a typed route
object:
// note the name of the route as a parameter
// which is necessary for TypeScript to know the type of the route
const route = useRoute('user');
console.log(route.params.id); // 👈 id is properly typed as a string!
console.log(route.params.other); // 👈 this throws an error
This is a nice addition even if manually defining the types is a bit cumbersome.
Note that if you are into file-based routing, you can use unplugin-vue-router which will automatically generate the types for you! The plugin is still experimental though.
create-vue
A new option has been added to create-vue
to allow you to use the new Devtools plugin,
a Vite plugin that allows you to use the Vue Devtools directly in the browser with an overlay.
npm create vue@latest my-app --devtools
Nuxt
Nuxt v3.12 is out and paves the way for Nuxt 4. You can try the changes using:
export default defineNuxtConfig({
future: {
compatibilityVersion: 4
}
})
That’s all for this release. Stay tuned for the next one!
Our ebook, online training and training are up-to-date with these changes if you want to learn more!