This article will introduce you to new features and paradigms introduced in Vue 3 and demonstrate how you can leverage the new Composition API to improve code expressiveness, as well as code organization and reuse.
Apart from admirable performance improvements, the recently released Vue 3 also brought several new features. Arguably the most important introduction is the Composition API. In the first part of this article, we recap the standard motivation for a new API: better code organization and reuse. In the second part, we will focus on less-discussed aspects of using the new API, such as implementing reactivity-based features that were inexpressible in Vue 2’s reactivity system.
We will refer to this as on-demand reactivity. After introducing the relevant new features, we will build a simple spreadsheet application to demonstrate the new expressiveness of Vue’s reactivity system. At the very end, we will discuss what real-world use this improvement on-demand reactivity might have.
Vue 3 is a major rewrite of Vue 2, introducing a plethora of improvements while retaining backward compatibility with the old API almost in its entirety.
One of the most significant new features in Vue 3 is the Composition API. Its introduction sparked much controversy when it was first discussed publicly. In case you are not already familiar with the new API, we will first describe the motivation behind it.
The usual unit of code organization is a JavaScript object whose keys represent various possible types of a piece of a component. Thus the object might have one section for reactive data (data
), another section for computed properties (computed
), one more for component methods (methods
), etc.
Under this paradigm, a component can have multiple unrelated or loosely related functionalities whose inner workings are distributed among the aforementioned component sections. For example, we might have a component for uploading files that implements two essentially separate functionalities: file management and a system that controls the upload status animation.
The <script>
portion might contain something like the following:
export default {
data () {
return {
animation_state: 'playing',
animation_duration: 10,
upload_filenames: [],
upload_params: {
target_directory: 'media',
visibility: 'private',
}
}
},
computed: {
long_animation () { return this.animation_duration > 5; },
upload_requested () { return this.upload_filenames.length > 0; },
},
...
}
There are benefits to this traditional approach to code organization, mainly in the developer not having to worry about where to write a new piece of code. If we’re adding a reactive variable, we insert it in the data
section. If we’re looking for an existing variable, we know it must be in the data
section.
This traditional approach of splitting the functionality’s implementation into sections (data
, computed
, etc.) is not suitable in all situations.
The following exceptions are cited often:
Vue 2 (and the backward-compatible Vue 3) offer a solution to most of the code organization and reuse issues: mixins.
Mixins allow the functionalities of a component to be extracted in a separate unit of code. Each functionality is put in a separate mixin and every component can use one or more mixins. Pieces defined in a mixin can be used in a component as if they were defined in the component itself. The mixins are a bit like classes in object-oriented languages in that they collect the code related to a given functionality. Like classes, mixins can be inherited (used) in other units of code.
However, reasoning with mixins is harder since, unlike classes, mixins need not be designed with encapsulation in mind. Mixins are allowed to be collections of loosely bound pieces of code without a well-defined interface to the outer world. Using more than one mixin at a time in the same component might result in a component that is difficult to comprehend and use.
Most object-oriented languages (e.g., C## and Java) discourage or even disallow multiple inheritance despite the fact that the object-oriented programming paradigm has the tools to deal with such complexity. (Some languages do allow multiple inheritance, such as C++, but composition is still preferred over inheritance.)
A more practical issue that may occur when using mixins in Vue is name collision, which occurs when using two or more mixins declaring common names. It should be noted here that if Vue’s default strategy for dealing with name collisions is not ideal in a given situation, the strategy can be adjusted by the developer.This comes at the cost of introducing more complexity.
Another issue is that mixins do not offer something akin to a class constructor. This is a problem because often we need functionality that is very similar, but not exactly the same, to be present in different components. This can be circumvented in some simple cases with the use of mixin factories.
Therefore, mixins are not the ideal solution for code organization and reuse, and the larger the project, the more serious their issues become. Vue 3 introduces a new way of solving the same issues concerning code organization and reuse.
The Composition API allows us (but does not require us) to completely decouple the pieces of a component. Every piece of code—a variable, a computed property, a watch, etc.—can be defined independently.
For example, instead of having an object that contains a data
section that contains a key animation_state
with the (default) value “playing,” we can now write (anywhere in our JavaScript code):
const animation_state = ref('playing');
The effect is almost the same as declaring this variable in the data
section of some component. The only essential difference is that we need to make the ref
defined outside of the component available in the component where we intend to use it. We do this by importing its module to the place where the component is defined and return the ref
from the setup
section of a component. We’ll skip this procedure for now and just focus on the new API for a moment. Reactivity in Vue 3 doesn’t require a component; it’s actually a self-contained system.
We can use the variable animation_state
in any scope that we import this variable to. After constructing a ref
, we get and set its actual value using ref.value
, for example:
animation_state.value = 'paused';
console.log(animation_state.value);
We need the ‘.value’ suffix since the assignment operator would otherwise assign the (non-reactive) value “paused” to the variable animation_state
. Reactivity in JavaScript (both when it is implemented through the defineProperty
as in Vue 2, and when it’s based on a Proxy
as in Vue 3) requires an object whose keys we can work with reactively.
Note that this was the case in Vue 2, as well; there, we had a component as a prefix to any reactive data member (component.data_member
). Unless and until the JavaScript language standard introduces the ability to overload the assignment operator, reactive expressions will require an object and a key (e.g., animation_state
and value
as above) to appear on the left-hand side of any assignment operation where we wish to preserve reactivity.
In templates, we can omit .value
since Vue has to preprocess the template code and can automatically detect references:
<animation :state='animation_state' />
In theory, the Vue compiler could preprocess the <script>
portion of a Single File Component (SFC) in a similar way, too, inserting .value
where needed. However, the use of refs
would then differ based on whether we are using SFCs or not, so perhaps such a feature is not even desirable.
Sometimes, we have an entity (for example, be a Javascript object or an array) that we never intend to replace with a completely different instance. Instead, we might only be interested in modifying its keyed fields. There is a shorthand in this case: using reactive
instead of ref
allows us to dispense with the .value
:
const upload_params = reactive({
target_directory: 'media',
visibility: 'private',
});
upload_params.visibility = 'public'; // no `.value` needed here
// if we did not make `upload_params` constant, the following code would compile but we would lose reactivity after the assignment; it is thus a good idea to make reactive variables ```const``` explicitly:
upload_params = {
target_directory: 'static',
visibility: 'public',
};
Decoupled reactivity with ref
and reactive
is not a completely new feature of Vue 3. It was partly introduced in Vue 2.6, where such decoupled instances of reactive data were called “observables.” For the most part, one can replace Vue.observable
with reactive
. One of the differences is that accessing and mutating the object passed to Vue.observable
directly is reactive, while the new API returns a proxy object, so mutating the original object will not have reactive effects.
What is completely new in Vue 3 is that other reactive pieces of a component can now be defined independently too, in addition to reactive data. Computed properties are implemented in an expected way:
const x = ref(5);
const x_squared = computed(() => x.value * x.value);
console.log(x_squared.value); // outputs 25
Similarly one can implement various types of watches, lifecycle methods, and dependency injection. For the sake of brevity, we won’t cover those here.
Suppose we use the standard SFC approach to Vue development. We might even be using the traditional API, with separate sections for data, computed properties, etc. How do we integrate the Composition API’s small bits of reactivity with SFCs? Vue 3 introduces another section just for this: setup
. The new section can be thought of as a new lifecycle method (which executes before any other hook—in particular, before created
).
Here is an example of a complete component that integrates the traditional approach with the Composition API:
<template>
<input v-model="x" />
<div>Squared: {{ x_squared }}, negative: {{ x_negative }}</div>
</template>
<script>
import { ref, computed } from 'vue';
export default {
name: "Demo",
computed: {
x_negative() { return -this.x; }
},
setup() {
const x = ref(0);
const x_squared = computed(() => x.value * x.value);
return {x, x_squared};
}
}
</script>
Things to take away from this example:
setup
. You might want to create a separate file for each functionality, import this file in an SFC, and return the desired bits of reactivity from setup
(to make them available to the remainder of the component).x
, even though it’s a reference, does not require .value
when referred to in the template code or in traditional sections of a component such as computed
.#vue #javascript #programming #web-development #developer