Key Takeaways
- Vue 3 recently released with plenty of new APIs that address pain points observed when using Vue at scale.
- The new composition API allows developer to package and reuse custom bits of logic across components. The composition API is entirely opt-in and does not replace the current options API.
- Vue 3's custom renderer API enables using Vue in non-DOM contexts (e.g., native, mobile, webGL).
- New Suspense, Teleport built-in components and CSS scoping rules add expressivity to Vue's template language.
- New fragment and custom events API let Vue components have a public API closer to that of regular DOM elements.
Vue 3 core was released in September last year. Vue 3 brings new core APIs, better performance, and improved TypeScript support. The new APIs include the composition API, custom event API, custom renderer API, the Suspense component, teleports, fragments, and new CSS scoping rules. Key participants in the Vue ecosystem (e.g., devtool, router, CLI, vue-test-utils, Vuex) are finishing their migration to Vue 3.
Composition API
The Vue Composition API achieves goals similar to those of React Hooks or Dojo middleware. Bits of logic can be encapsulated in functions, reused and composed, potentially lowering code size, and thus performance. The new Composition API also helps maintainability by letting developers group concerns into a single composition function (the analog to a hook in a React context). The Composition API additionally has the side-effect of enabling better type inference with TypeScript. The Vue documentation recommends the new composition API for large codebases:
Creating Vue components allows us to extract repeatable parts of the interface coupled with its functionality into reusable pieces of code. This alone can get our application pretty far in terms of maintainability and flexibility. However, our collective experience has proved that this alone might not be enough, especially when your application is getting really big – think several hundred components. When dealing with such large applications, sharing and reusing code becomes especially important.
On smaller codebases, developers may still continue the standard component API (available in Vue 2 and referred to as the Options API) or instead leverage the benefits of the new composition API. The new composition API is thus entirely opt-in. This enables developers to adopt a progressive approach to learning the intricacies of Vue 3, without affecting their ability to develop full-featured applications. This, in turn, fits Vue’s self-description as the progressive JavaScript framework.
The composition API relies on a setup
function that acts as a constructor or factory which returns entities that are relevant to and put in scope of a component. The setup factory may register lifecycle callbacks (e.g., onMounted
) that mirror those available in the options API (e.g., mounted
). The setup factory may also define reactive computations that specify the component behavior — ref
API to define atomic reactive values, watch
and watchEffect
APIs to define dependencies and run effects when those dependencies update, computed
API to run pure computations when dependencies update.
The setup factory can be broken into as many factories as desired by the developer, with the values returned by those factories being merged to form the final setup output. Each of these factories reminisces of a React hook. However, the setup factory only runs once in the lifetime of a component. Vue 3 thus achieves similar functionality as React hooks without the arguable awkwardness and accidental complexity (e.g., React hook rules, eslint plugin, stale data) that come from hooks running multiple times.
A step-by-step example of usage of the composition API is available in Vue 3’s documentation.
Custom events
A Vue component can be parameterized via attributes (props), event listeners, and dependency injection. The custom event API lets developers define the event interface of a Vue component. Vue 3 requires developers to explicitly specify the event interface of a child component to bind listeners for those events on the parent component. The custom events API allows declaring the events that a component emits by means of a emits
property, and optionally providing an event validation function:
app.component('custom-form', {
emits: {
// No validation
click: null,
// Validate submit event
submit: ({ email, password }) => {
if (email && password) {
return true
} else {
console.warn('Invalid submit event payload!')
return false
}
}
},
methods: {
submitForm() {
this.$emit('submit', { email, password })
}
}
})
The <custom-form>
component defined thereabove can have a listener provisioned for the submit
event with the usual Vue syntax (<custom-form @submit="..."
). Like in Vue 2.x, Vue 3 custom events do not bubble. The Vue 3 documentation provides an example of usage with forms.
Chris Fritz showed at VueConfUS how custom events can be used to write so-called transparent wrapper components. While Vue components will likely never be reasoned about as regular elements the way that web components allow to, Vue 3 allows narrowing the gap.
Fritz showcased how an input element:
<input v-model="searchText" placeholder="Search" @keyup.enter="search"/>
could be wrapped into a <BaseInput>
Vue component:
<BaseInput v-model="searchText" placeholder="Search" @keyup.enter="search"/>
in a far easier way that previously possible with Vue 2:
<script>
export default { props: ['label'] }`
</script>
<template>
<label> {{ label }}
<input v-bind="$attrs" />
</label>
</template>
While Fritz’s presentation uses an outdated version of Vue 3, and a few internal implementation details of the BaseInput
component may have changed, the component’s external API remains the same and mirrors that available on a regular <input>
element.
The placeholder
attribute and the enter key event listener of the BaseInput
Vue component are bound on the <input>
element of the BaseInput
component template with v-bind="$attrs"
. The label
attribute does not figure in the $attrs
object as it is explicitly declared as a prop of the BaseInput
component. The placeholder
attribute is called a non-prop attribute. All non-prop attributes are available in the $attrs
object.
Custom renderer
The custom renderer API (createRenderer
method) allows using Vue in non-DOM contexts by customizing the rendering methods. The method is a factory that returns a render function that can be used as usual in a Vue codebase:
import { createRenderer } from 'vue'
const { render, createApp } = createRenderer<Node, Element>({
patchProp,
...nodeOps
})
The API is currently scantly documented and is oriented to fairly advanced Vue library authors that want to target non-DOM platforms. It provides a functionality similar to that of the react-reconciler package used to implement React renderers for native platforms, canvas, pdf, and more. Evan You, Vue’s creator, discussed in a talk in April last year ongoing user-led efforts to implement custom renderers thanks to Vue 3’s custom renderer API — @vue/compiler-ssr for server-side rendering, NativeScript/Vue integration for mobile platforms, vuminal for terminals, vuegl WebGL custom renderer. The custom renderer can be used in combination with the new Vue 3 template compiler. You explained at Vue Amsterdam that the new template compiler fully supports source maps and user-defined transform plug-ins. The compiler additionally follows a layered design which enables the creation of more complex compilers on top of Vue 3’s.
(Example of usage of vuminal application. Source: vuminal’s GitHub repository)
Fragments
Vue fragments add support for multi-root node components:
<custom-layout id="custom-layout" @click="changeValue">
</custom-layout>
// This will raise a warning
app.component('custom-layout', {
template: `
<header>...</header>
<main>...</main>
<footer>...</footer>
`
})
// No warnings, $attrs are passed to <main> element
app.component('custom-layout', {
template: `
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
`
})
The custom-layout
component has three nodes in its template. The custom-layout
component props (id and click event handler) will be bound on the main
component annotated with v-bind="$attrs"
. Manual binding of fragments attributes allows distributing a fragment component’s non-props attributes anywhere needed in the component template. As previously mentioned, non-prop attributes are attributes that are not a part of the explicitly provided input interface of a component — e.g., attributes not defined in the component’s props property, or listeners on events not defined in an emits property.
Teleport
<Teleport>
s involve rendering children components into a DOM node that exists outside the DOM hierarchy of their parent component. Teleports can thus be used for modals, dropdowns, or notifications.
The Vue 3 documentation provides the following example of API usage to implement modals:
app.component('modal-button', {
template: `
<button @click="modalOpen = true">
Open full screen modal! (With teleport!)
</button>
<teleport to="body">
<div v-if="modalOpen" class="modal">
<div>
I'm a teleported modal!
(My parent is "body")
<button @click="modalOpen = false">
Close
</button>
</div>
</div>
</teleport>
`,
data() {
return {
modalOpen: false
}
}
})
The <teleport>
built-in component will render the modal’s content as a child of the body
tag (<teleport to="body">
).
Suspense
<Suspense>
is a new Vue built-in element that is designed to work with asynchronous components. The Vue documentation explains:
Async components are suspensible by default. This means if it has an
<Suspense>
in the parent chain, it will be treated as an async dependency of that<Suspense>
. In this case, the loading state will be controlled by the<Suspense>
, and the component’s own loading, error, delay, and timeout options will be ignored.The async component can opt-out of
Suspense
control and let the component always control its own loading state by specifyingsuspensible: false
in its options.
Vue 3’s release note advanced that the new Suspense component remains somewhat experimental and will be documented further when it is stable:
We have also implemented a currently undocumented
<Suspense>
component, which allows waiting on nested async dependencies (async components or components withasync setup()
) on initial render or branch switch. We are testing and iterating on this feature with the Nuxt.js team (Nuxt 3 is on the way) and will likely solidify it in 3.1.
New CSS scoping rules
Vue single-file components (SFC) define styles that are scoped to themselves by default. SFC components can now specify global rules or rules that target only slotted content. The new features use the <style scoped>
syntax:
<style scoped>
/* deep selectors */
::v-deep(.foo) {}
/* shorthand */
:deep(.foo) {}
/* targeting slot content */
::v-slotted(.foo) {}
/* shorthand */
:slotted(.foo) {}
/* one-off global rule */
::v-global(.foo) {}
/* shorthand */
:global(.foo) {}
</style>
The newly refined scoping abilities help address a few commonly occurring cases that motivated the CSS Scoping Module Level 1 and CSS Shadow Parts proposal. Custom elements’ shadow DOM allows developers to separate pages into subtrees of markup whose details are only relevant to the component itself, not the outside page. The CSS Shadow Parts specification explained the tradeoffs associated with the isolation of custom elements:
This reduces the chance of a style meant for one part of the page accidentally over-applying and making a different part of the page look wrong. However, this styling barrier also makes it harder for a page to interact with its components when it actually wants to do so.
Migration and ecosystem
Developers can review the Vue 3 docs online, along with a migration guide. Important pieces of the Vue ecosystem are finishing their migration to Vue 3 and a few may still be in the beta/RC stage. Vue’s official router library has released its v4 and now supports Vue 3. The Vue developer tool is still in beta. The official testing suite vue-test-utils v2 that targets Vue 3 is also in beta. State management library Vuex is in the release candidate stage. Vuex 4 will be Vue 3 compatible while providing the same API as Vuex 3.
Vue 3’s documentation warned:
We are still working on a dedicated Migration Build of Vue 3 with Vue 2 compatible behavior and runtime warnings of incompatible usage. If you are planning to migrate a non-trivial Vue 2 app, we strongly recommend waiting for the Migration Build for a smoother experience.
Final remarks
Vue 3 is a huge release that represents more than 2 years of development efforts that involved around a hundred contributors and more than 30 RFCs. With version 3, Vue provides features that make Vue allegedly more performant and arguably more modular. Vue 3 features important additions to support using Vue at scale (composition API) and outside DOM environments. Vue 3 reflects the maturing of the framework and its ambition to be a solid technological choice for a larger and more varied set of applications.
So far, Vue has shown a certain ability to avoid making alienating choices. It for instance prominently favors HTML-like, template-based SFC components, at the same time offering the possibility to use full-JavaScript render functions (including with JSX). Vue 3 keeps Vue 2’s option API while adding the composition API for larger applications with hundreds of components.
Growth however comes with its set of problems. The release in June 2019 of the composition API’s RFC gave birth to an animated discussion within the developer community. Some developers expressed their fear that Vue, in its attempt to better address the needs of large applications, was reaching a complexity threshold that made it less attractive to the large developer base targeting average-size applications. Those concerns may have been assuaged by the entirely opt-in nature of the composition API, itself habilitated by a modular architecture that reduces the cost of unused modules. However, the tension that comes from addressing with equal quality different segments with different needs in a single package may only grow. It will be interesting to see how that tension drives the framework forward.
About the Author
Bruno Couriol holds a Msc in Telecommunications, a BsC in Mathematics and a MBA by INSEAD. Starting with Accenture, most of his career has been spent as a consultant, helping large companies addressing their critical strategical, organizational and technical issues. In the last few years, he developed a focus on the intersection of business, technology and entrepreneurship.