authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
Luka Mikec's profile image

Luka Mikec

Luka has extensive experience in academia and full-stack software development. He is a cotutelle Ph.D. candidate in mathematics and computer science.

Years of Experience

15

Share

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

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.

Reactivity is More Expressive in Vue 3

In the first part of this article, we touched upon the standard motivation for the Composition API, which is improved code organization and reuse. Indeed, the new API’s main selling point is not its power, but the organizational convenience that it brings: the ability to structure the code more clearly. It might seem like that is all—that the Composition API enables a way of implementing components that avoids the limitations of the already existing solutions, such as mixins.

However, there is more to the new API. The Composition API actually enables not just better organized but more powerful reactive systems. The key ingredient is the ability to dynamically add reactivity to the application. Previously, one had to define all the data, all the computed properties, etc. before loading a component. Why would adding reactive objects at a later stage be useful? In what remains we take a look at a more complex example: spreadsheets.

Creating a Spreadsheet in Vue 2

Spreadsheet tools such as Microsoft Excel, LibreOffice Calc, and Google Sheets all have some sort of a reactivity system. These tools present a user with a table, with columns indexed by A–Z, AA–ZZ, AAA–ZZZ, etc., and rows indexed numerically.

Each cell may contain a plain value or a formula. A cell with a formula is essentially a computed property, which may depend on values or other computed properties. With standard spreadsheets (and unlike the reactivity system in Vue), these computed properties are even allowed to depend on themselves! Such self-reference is useful in some scenarios where the desired value is obtained by iterative approximation.

Once a cell’s content changes, all the cells that depend on the cell in question will trigger an update. If further changes occur, further updates might be scheduled.

If we were to build a spreadsheet application with Vue, it would be natural to ask if we can put Vue’s own reactivity system to use and make Vue the engine of a spreadsheet app. For each cell, we could remember its raw editable value, as well as the corresponding computed value. Computed values would reflect the raw value if it’s a plain value, and otherwise, the computed values are the result of the expression (formula) that is written instead of a plain value.

With Vue 2, a way to implement a spreadsheet is to have raw_values a two-dimensional array of strings, and computed_values a (computed) two-dimensional array of cell values.

If the number of cells is small and fixed before the appropriate Vue component loads, we could have one raw and one computed value for every cell of the table in our component definition. Aside from the aesthetic monstrousness that such an implementation would cause, a table with a fixed number of cells at compile-time probably doesn’t count as a spreadsheet.

There are problems with the two-dimensional array computed_values, too. A computed property is always a function whose evaluation, in this case, depends on itself (calculating the value of a cell will, in general, require some other values to already be computed). Even if Vue allowed self-referential computed properties, updating a single cell would cause all cells to be recomputed (regardless of whether there are dependencies or not). This would be extremely inefficient. Thus, we might end up using reactivity to detect changes in the raw data with Vue 2, but everything else reactivity-wise would have to be implemented from scratch.

Modeling Computed Values in Vue 3

With Vue 3, we can introduce a new computed property for every cell. If the table grows, new computed properties are introduced.

Suppose we have cells A1 and A2, and we wish for A2 to display the square of A1 whose value is the number 5. A sketch of this situation:

let A1 = computed(() => 5);
let A2 = computed(() => A1.value * A1.value);
console.log(A2.value); // outputs 25

Suppose we stay in this simple scenario for a moment. There is an issue here; what if we wish to change A1 so that it contains the number 6? Suppose we write this:

A1 = computed(() => 6);
console.log(A2.value); // outputs 25 if we already ran the code above

This didn’t merely change the value 5 to 6 in A1. The variable A1 has a completely different identity now: the computed property that resolves to the number 6. However, the variable A2 still reacts to changes of the old identity of the variable A1. So, A2 shouldn’t refer to A1 directly, but rather to some special object that will always be available in the context, and will tell us what is A1 at the moment. In other words, we need a level of indirection before accessing A1, something like a pointer. There are no pointers as first-class entities in Javascript, but it’s easy to simulate one. If we wish to have a pointer pointing to a value, we can create an object pointer = {points_to: value}. Redirecting the pointer amounts to assigning to pointer.points_to, and dereferencing (accessing the pointed-to value) amounts to retrieving the value of pointer.points_to. In our case we proceed as follows:

let A1 = reactive({points_to: computed(() => 5)});
let A2 = reactive({points_to: computed(() => A1.points_to * A1.points_to)});
console.log(A2.points_to); // outputs 25

Now we can substitute 5 with 6.

A1.points_to = computed(() => 6);
console.log(A2.points_to); // outputs 36

On Vue’s Discord server, the user redblobgames suggested another interesting approach: instead of using computed values, use references that wrap regular functions. This way, one can similarly swap the function without changing the identity of the reference itself.

Our spreadsheet implementation will have cells referred to by keys of some two-dimensional array. This array can provide the level of indirection we require. Thus in our case, we won’t require any additional pointer simulation. We could even have one array that does not distinguish between raw and computed values. Everything can be a computed value:

const cells = reactive([
  computed(() => 5),
  computed(() => cells[0].value * cells[0].value)
]);

cells[0] = computed(() => 6);
console.log(cells[1].value); // outputs 36

However, we really want to distinguish raw and computed values since we want to be able to bind the raw value to an HTML input element. Furthermore, if we have a separate array for raw values, we never have to change the definitions of computed properties; they will update automatically based on the raw data.

Implementing the Spreadsheet

Let’s start with some basic definitions, which are for the most part self-explanatory.

const rows = ref(30), cols = ref(26);

/* if a string codes a number, return the number, else return a string */
const as_number = raw_cell => /^[0-9]+(\.[0-9]+)?$/.test(raw_cell)  
    ?  Number.parseFloat(raw_cell)  :  raw_cell;

const make_table = (val = '', _rows = rows.value, _cols = cols.value) =>
    Array(_rows).fill(null).map(() => Array(_cols).fill(val));

const raw_values = reactive(make_table('', rows.value, cols.value));
const computed_values = reactive(make_table(undefined, rows.value, cols.value));

/* a useful metric for debugging: how many times did cell (re)computations occur? */
const calculations = ref(0);

The plan is for every computed_values[row][column] to be computed as follows. If raw_values[row][column] doesn’t start with =, return raw_values[row][column]. Otherwise, parse the formula, compile it to JavaScript, evaluate the compiled code, and return the value. To keep things short, we’ll cheat a bit with parsing formulas and we won’t do some obvious optimizations here, such as a compilation cache.

We will assume that users can enter any valid JavaScript expression as a formula. We can replace references to cell names that appear in user’s expressions, such as A1, B5, etc., with the reference to the actual cell value (computed). The following function does this job, assuming that strings resembling cell names really always identify cells (and are not a part of some unrelated JavaScript expression). For simplicity, we will assume column indices consist of a single letter.

const letters = Array(26).fill(0)
    .map((_, i) => String.fromCharCode("A".charCodeAt(0) + i));

const transpile = str => {
    let cell_replacer = (match, prepend, col, row) => {
        col = letters.indexOf(col);
        row = Number.parseInt(row) - 1;
        return prepend + ` computed_values[${row}][${col}].value `;
    };
    return str.replace(/(^|[^A-Z])([A-Z])([0-9]+)/g, cell_replacer);
};

Using the transpile function, we can get pure JavaScript expressions out of expressions written in our little “extension” of JavaScript with cell references.

The next step is to generate computed properties for every cell. This procedure will occur once in the lifetime of every cell. We can make a factory that will return the desired computed properties:

const computed_cell_generator = (i, j) => {
    const computed_cell = computed(() => {
        // we don't want Vue to think that the value of a computed_cell depends on the value of `calculations`
        nextTick(() => ++calculations.value);
      
        let raw_cell = raw_values[i][j].trim();
        if (!raw_cell || raw_cell[0] != '=') 
            return as_number(raw_cell);
      
        let user_code = raw_cell.substring(1);
        let code = transpile(user_code);
        try {
            // the constructor of a Function receives the body of a function as a string
            let fn = new Function(['computed_values'], `return ${code};`);
            return fn(computed_values);
        } catch (e) {
            return "ERROR";
        }
    });
    return computed_cell;
};

for (let i = 0; i < rows.value; ++i)
    for (let j = 0; j < cols.value; ++j)
        computed_values[i][j] = computed_cell_generator(i, j);

If we put all of the code above in the setup method, we need to return {raw_values, computed_values, rows, cols, letters, calculations}.

Below, we present the complete component, together with a basic user interface.

The code is available on GitHub, and you can also check out the live demo.






What About Real-world Use?

We saw how the decoupled reactivity system of Vue 3 enables not only cleaner code but allows more complex reactive systems based on the Vue’s new reactivity mechanism. Roughly seven years have passed since Vue was introduced, and the rise in expressiveness was clearly not highly sought after.

The spreadsheet example is a straightforward demonstration of what Vue is capable of now, and you can also check out the live demo.

But as a real-word example, it is somewhat niche. In what sort of situations might the new system come in handy? The most obvious use-case for on-demand reactivity might be in performance gains for complex applications.

Vue 2 vs Vue 3 funnel comparison.

In front-end applications that work with a large amount of data, the overhead of using poorly thought-through reactivity might have a negative impact on performance. Suppose we have a business dashboard application that produces interactive reports of the company’s business activity. The user can select a time range and add or remove performance indicators in the report. Some indicators may display values that depend on other indicators.

One way to implement report generation is through a monolithic structure. When the user changes an input parameter in the interface, a single computed property, e.g., report_data, gets updated. The computation of this computed property happens according to a hardcoded plan: first, calculate all the independent performance indicators, then those that depend only on these independent indicators, etc.

A better implementation will decouple bits of the report and compute them independently. There are some benefits to this:

  • The developer does not have to hardcode an execution plan, which is tedious and error-prone. Vue’s reactivity system will automatically detect dependencies.
  • Depending on the amount of data involved, we might get substantial performance gains since we are only updating the report data that logically depended on the modified input parameters.

If all performance indicators that may be a part of the final report are known before the Vue component gets loaded, we may be able to implement the proposed decoupling even with Vue 2. Otherwise, if the backend is the single source of truth (which is usually the case with data-driven applications), or if there are external data providers, we can generate on-demand computed properties for every piece of a report.

Thanks to Vue 3, this is now not only possible but easy to do.

Understanding the basics

  • What is the latest Vue?

    Vue 3, codenamed “One Piece,” is the latest release.

  • Is Vue 3 stable?

    Vue 3 is officially stable. However, while writing the code examples presented in this article, I encountered and reported some minor issues.

  • Is Vue 3 backwards compatible?

    Yes, in the sense that it does not require substantial changes to your code. However, many apps will require some minor changes.

  • How old is Vue?

    Vue was first made public in February 2014. Vue 1.0 was released in October 2015, and the latest version (Vue 3.0) was released in September 2020.

Consult the author or an expert on this topic.
Schedule a call
Luka Mikec's profile image
Luka Mikec

Located in Zagreb, Croatia

Member since September 3, 2020

About the author

Luka has extensive experience in academia and full-stack software development. He is a cotutelle Ph.D. candidate in mathematics and computer science.

Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

Years of Experience

15

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

Toptal Developers

Join the Toptal® community.

\n\n\n

Things to take away from this example:

\n\n\n\n

Reactivity is More Expressive in Vue 3

\n\n

In the first part of this article, we touched upon the standard motivation for the Composition API, which is improved code organization and reuse. Indeed, the new API’s main selling point is not its power, but the organizational convenience that it brings: the ability to structure the code more clearly. It might seem like that is all—that the Composition API enables a way of implementing components that avoids the limitations of the already existing solutions, such as mixins.

\n\n

However, there is more to the new API. The Composition API actually enables not just better organized but more powerful reactive systems. The key ingredient is the ability to dynamically add reactivity to the application. Previously, one had to define all the data, all the computed properties, etc. before loading a component. Why would adding reactive objects at a later stage be useful? In what remains we take a look at a more complex example: spreadsheets.

\n\n

Creating a Spreadsheet in Vue 2

\n\n

Spreadsheet tools such as Microsoft Excel, LibreOffice Calc, and Google Sheets all have some sort of a reactivity system. These tools present a user with a table, with columns indexed by A–Z, AA–ZZ, AAA–ZZZ, etc., and rows indexed numerically.

\n\n

Each cell may contain a plain value or a formula. A cell with a formula is essentially a computed property, which may depend on values or other computed properties. With standard spreadsheets (and unlike the reactivity system in Vue), these computed properties are even allowed to depend on themselves! Such self-reference is useful in some scenarios where the desired value is obtained by iterative approximation.

\n\n

Once a cell’s content changes, all the cells that depend on the cell in question will trigger an update. If further changes occur, further updates might be scheduled.

\n\n

If we were to build a spreadsheet application with Vue, it would be natural to ask if we can put Vue’s own reactivity system to use and make Vue the engine of a spreadsheet app. For each cell, we could remember its raw editable value, as well as the corresponding computed value. Computed values would reflect the raw value if it’s a plain value, and otherwise, the computed values are the result of the expression (formula) that is written instead of a plain value.

\n\n

With Vue 2, a way to implement a spreadsheet is to have raw_values a two-dimensional array of strings, and computed_values a (computed) two-dimensional array of cell values.

\n\n

If the number of cells is small and fixed before the appropriate Vue component loads, we could have one raw and one computed value for every cell of the table in our component definition. Aside from the aesthetic monstrousness that such an implementation would cause, a table with a fixed number of cells at compile-time probably doesn’t count as a spreadsheet.

\n\n

There are problems with the two-dimensional array computed_values, too. A computed property is always a function whose evaluation, in this case, depends on itself (calculating the value of a cell will, in general, require some other values to already be computed). Even if Vue allowed self-referential computed properties, updating a single cell would cause all cells to be recomputed (regardless of whether there are dependencies or not). This would be extremely inefficient. Thus, we might end up using reactivity to detect changes in the raw data with Vue 2, but everything else reactivity-wise would have to be implemented from scratch.

\n\n

Modeling Computed Values in Vue 3

\n\n

With Vue 3, we can introduce a new computed property for every cell. If the table grows, new computed properties are introduced.

\n\n

Suppose we have cells A1 and A2, and we wish for A2 to display the square of A1 whose value is the number 5. A sketch of this situation:

\n\n
let A1 = computed(() => 5);\nlet A2 = computed(() => A1.value * A1.value);\nconsole.log(A2.value); // outputs 25\n
\n\n

Suppose we stay in this simple scenario for a moment. There is an issue here; what if we wish to change A1 so that it contains the number 6? Suppose we write this:

\n\n
A1 = computed(() => 6);\nconsole.log(A2.value); // outputs 25 if we already ran the code above\n
\n\n

This didn’t merely change the value 5 to 6 in A1. The variable A1 has a completely different identity now: the computed property that resolves to the number 6. However, the variable A2 still reacts to changes of the old identity of the variable A1. So, A2 shouldn’t refer to A1 directly, but rather to some special object that will always be available in the context, and will tell us what is A1 at the moment. In other words, we need a level of indirection before accessing A1, something like a pointer. There are no pointers as first-class entities in Javascript, but it’s easy to simulate one. If we wish to have a pointer pointing to a value, we can create an object pointer = {points_to: value}. Redirecting the pointer amounts to assigning to pointer.points_to, and dereferencing (accessing the pointed-to value) amounts to retrieving the value of pointer.points_to. In our case we proceed as follows:

\n\n
let A1 = reactive({points_to: computed(() => 5)});\nlet A2 = reactive({points_to: computed(() => A1.points_to * A1.points_to)});\nconsole.log(A2.points_to); // outputs 25\n
\n\n

Now we can substitute 5 with 6.

\n\n
A1.points_to = computed(() => 6);\nconsole.log(A2.points_to); // outputs 36\n
\n\n

On Vue’s Discord server, the user redblobgames suggested another interesting approach: instead of using computed values, use references that wrap regular functions. This way, one can similarly swap the function without changing the identity of the reference itself.

\n\n

Our spreadsheet implementation will have cells referred to by keys of some two-dimensional array. This array can provide the level of indirection we require. Thus in our case, we won’t require any additional pointer simulation. We could even have one array that does not distinguish between raw and computed values. Everything can be a computed value:

\n\n
const cells = reactive([\n  computed(() => 5),\n  computed(() => cells[0].value * cells[0].value)\n]);\n\ncells[0] = computed(() => 6);\nconsole.log(cells[1].value); // outputs 36\n
\n\n

However, we really want to distinguish raw and computed values since we want to be able to bind the raw value to an HTML input element. Furthermore, if we have a separate array for raw values, we never have to change the definitions of computed properties; they will update automatically based on the raw data.

\n\n

Implementing the Spreadsheet

\n\n

Let’s start with some basic definitions, which are for the most part self-explanatory.

\n\n
const rows = ref(30), cols = ref(26);\n\n/* if a string codes a number, return the number, else return a string */\nconst as_number = raw_cell => /^[0-9]+(\\.[0-9]+)?$/.test(raw_cell)  \n    ?  Number.parseFloat(raw_cell)  :  raw_cell;\n\nconst make_table = (val = '', _rows = rows.value, _cols = cols.value) =>\n    Array(_rows).fill(null).map(() => Array(_cols).fill(val));\n\nconst raw_values = reactive(make_table('', rows.value, cols.value));\nconst computed_values = reactive(make_table(undefined, rows.value, cols.value));\n\n/* a useful metric for debugging: how many times did cell (re)computations occur? */\nconst calculations = ref(0);\n
\n\n

The plan is for every computed_values[row][column] to be computed as follows. If raw_values[row][column] doesn’t start with =, return raw_values[row][column]. Otherwise, parse the formula, compile it to JavaScript, evaluate the compiled code, and return the value. To keep things short, we’ll cheat a bit with parsing formulas and we won’t do some obvious optimizations here, such as a compilation cache.

\n\n

We will assume that users can enter any valid JavaScript expression as a formula. We can replace references to cell names that appear in user’s expressions, such as A1, B5, etc., with the reference to the actual cell value (computed). The following function does this job, assuming that strings resembling cell names really always identify cells (and are not a part of some unrelated JavaScript expression). For simplicity, we will assume column indices consist of a single letter.

\n\n
const letters = Array(26).fill(0)\n    .map((_, i) => String.fromCharCode(\"A\".charCodeAt(0) + i));\n\nconst transpile = str => {\n    let cell_replacer = (match, prepend, col, row) => {\n        col = letters.indexOf(col);\n        row = Number.parseInt(row) - 1;\n        return prepend + ` computed_values[${row}][${col}].value `;\n    };\n    return str.replace(/(^|[^A-Z])([A-Z])([0-9]+)/g, cell_replacer);\n};\n
\n\n

Using the transpile function, we can get pure JavaScript expressions out of expressions written in our little “extension” of JavaScript with cell references.

\n\n

The next step is to generate computed properties for every cell. This procedure will occur once in the lifetime of every cell. We can make a factory that will return the desired computed properties:

\n\n
const computed_cell_generator = (i, j) => {\n    const computed_cell = computed(() => {\n        // we don't want Vue to think that the value of a computed_cell depends on the value of `calculations`\n        nextTick(() => ++calculations.value);\n      \n        let raw_cell = raw_values[i][j].trim();\n        if (!raw_cell || raw_cell[0] != '=') \n            return as_number(raw_cell);\n      \n        let user_code = raw_cell.substring(1);\n        let code = transpile(user_code);\n        try {\n            // the constructor of a Function receives the body of a function as a string\n            let fn = new Function(['computed_values'], `return ${code};`);\n            return fn(computed_values);\n        } catch (e) {\n            return \"ERROR\";\n        }\n    });\n    return computed_cell;\n};\n\nfor (let i = 0; i < rows.value; ++i)\n    for (let j = 0; j < cols.value; ++j)\n        computed_values[i][j] = computed_cell_generator(i, j);\n
\n\n

If we put all of the code above in the setup method, we need to return {raw_values, computed_values, rows, cols, letters, calculations}.

\n\n

Below, we present the complete component, together with a basic user interface.

\n\n

The code is available on GitHub, and you can also check out the live demo.

\n\n
\n\n\n\n\n
\n\n

What About Real-world Use?

\n\n

We saw how the decoupled reactivity system of Vue 3 enables not only cleaner code but allows more complex reactive systems based on the Vue’s new reactivity mechanism. Roughly seven years have passed since Vue was introduced, and the rise in expressiveness was clearly not highly sought after.

\n\n

The spreadsheet example is a straightforward demonstration of what Vue is capable of now, and you can also check out the live demo.

\n\n

But as a real-word example, it is somewhat niche. In what sort of situations might the new system come in handy? The most obvious use-case for on-demand reactivity might be in performance gains for complex applications.

\n\n
\n \"Vue\n
\n\n

In front-end applications that work with a large amount of data, the overhead of using poorly thought-through reactivity might have a negative impact on performance. Suppose we have a business dashboard application that produces interactive reports of the company’s business activity. The user can select a time range and add or remove performance indicators in the report. Some indicators may display values that depend on other indicators.

\n\n

One way to implement report generation is through a monolithic structure. When the user changes an input parameter in the interface, a single computed property, e.g., report_data, gets updated. The computation of this computed property happens according to a hardcoded plan: first, calculate all the independent performance indicators, then those that depend only on these independent indicators, etc.

\n\n

A better implementation will decouple bits of the report and compute them independently. There are some benefits to this:

\n\n\n\n

If all performance indicators that may be a part of the final report are known before the Vue component gets loaded, we may be able to implement the proposed decoupling even with Vue 2. Otherwise, if the backend is the single source of truth (which is usually the case with data-driven applications), or if there are external data providers, we can generate on-demand computed properties for every piece of a report.

\n\n

Thanks to Vue 3, this is now not only possible but easy to do.

\n","as":"div","isContentFit":true,"sharingWidget":{"url":"http://g1f.4dian8.com/vue-js/on-demand-reactivity-vue-3","title":"On-demand Reactivity in Vue 3","text":null,"providers":["linkedin","twitter","facebook"],"gaCategory":null,"domain":{"name":"developers","title":"Engineering","vertical":{"name":"developers","title":"Developers","publicUrl":"http://g1f.4dian8.com/developers"},"publicUrl":"http://g1f.4dian8.com/developers/blog"},"hashtags":"Vue3,Vue,CompositionAPI"}}