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.

What’s New in Vue 3 and Why it Matters

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:

  1. Dealing with a component with a large number of functionalities. If we want to upgrade our animation code with the ability to delay the start of the animation, for example, we will have to scroll/jump between all the relevant sections of the component in a code editor. In the case of our file-uploading component, the component itself is small and the number of functionalities it implements is small, too. Thus, in this case, jumping between the sections is not really a problem. This issue of code fragmentation becomes relevant when we deal with large components.
  2. Another situation where the traditional approach is lacking is code reuse. Often we need to make a specific combination of reactive data, computed properties, methods, etc., available in more than one component.

Vue 2 (and the backward-compatible Vue 3) offer a solution to most of the code organization and reuse issues: mixins.

Pros and Cons of Mixins in Vue 3

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.

Composition API: Vue 3’s Answer to Code Organisation 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.

Comparison: Options API vs. Composition API.

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:

  • All the Composition API code is now in 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).
  • You can mix the new and the traditional approach in the same file. Notice that 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.
  • Last but not least, notice we have two root DOM nodes in our template; the ability to have multiple root nodes is another new feature of Vue 3.

#vue #javascript #programming #web-development #developer

On-demand Reactivity in Vue 3
17.30 GEEK