Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Vue.js 3 Design Patterns and Best Practices

You're reading from   Vue.js 3 Design Patterns and Best Practices Develop scalable and robust applications with Vite, Pinia, and Vue Router

Arrow left icon
Product type Paperback
Published in May 2023
Publisher Packt
ISBN-13 9781803238074
Length 296 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Author (1):
Arrow left icon
Pablo David Garaguso Pablo David Garaguso
Author Profile Icon Pablo David Garaguso
Pablo David Garaguso
Arrow right icon
View More author details
Toc

Table of Contents (16) Chapters Close

Preface 1. Chapter 1: The Vue 3 Framework 2. Chapter 2: Software Design Principles and Patterns FREE CHAPTER 3. Chapter 3: Setting Up a Working Project 4. Chapter 4: User Interface Composition with Components 5. Chapter 5: Single-Page Applications 6. Chapter 6: Progressive Web Applications 7. Chapter 7: Data Flow Management 8. Chapter 8: Multithreading with Web Workers 9. Chapter 9: Testing and Source Control 10. Chapter 10: Deploying Your Application 11. Chapter 11: Bonus Chapter - UX Patterns 12. Final words 13. Index 14. Other Books You May Enjoy Appendix: Migrating from Vue 2

A quick reference list of patterns

Patterns are classified according to the type of function or problem they solve. There are plenty of patterns according to the context, language, and architecture of a system. Here is a non-exclusive list of patterns that we will use throughout this book and that, in my experience, are more likely to appear in Vue applications:

  • Creational patterns: These deal with the approach to creating classes, objects, and data structures:
    • Singleton pattern
    • Dependency injection pattern
    • Factory pattern
  • Behavioral patterns: These deal with communication between objects, components, and other elements of the application:
    • Observer pattern
    • Command pattern
  • Structural patterns: These provide templates that affect the design of your application and the relationship between components:
    • Proxy pattern
    • Decorator pattern
    • Façade pattern
  • Asynchronous patterns: These deal with data and process flow with asynchronous requests and events in single-threaded applications (heavily used in web applications):
    • Callbacks pattern
    • Promises pattern

Not by any means this list of patterns is exclusive. There are many more patterns and classifications, and a full library is dedicated to this subject. It is worth mentioning that the description and application for some of these may differ from one literature to another and there is some overlapping depending on the context and implementation.

With that introduction to design patterns, let’s look at them in detail with examples.

The singleton pattern

This is a very common pattern in JavaScript and perhaps one of, if not the most important. The basic concept defines that one object’s instance must only exist once in the entire application, and all references and function calls are done through this object. A singleton can act as a gateway to resources, libraries, and data.

When to use it

Here is a short rule of thumb to know when to apply this pattern:

  • When you need to make sure a resource is accessed through only one gateway, for example, the global application state
  • When you need to encapsulate or simplify behavior or communications (used in conjunction with other patterns). For example, the API access object.
  • When the cost of multiple instantiations is detrimental. For example, the creation of web workers.

Implementations

There are many ways that you can apply this pattern in JavaScript. In some cases, the implementation from other languages is migrated to JavaScript, often following Java examples with the use of a getInstance() method to obtain the singleton. However, there are better ways to implement this pattern in JavaScript. Let’s see them next.

Method 1

The simplest way is through a module that exports a plain object literal or a JavaScript Object Notation (JSON), which is a static object:

./chapter 2/singleton-json.js

const my_singleton={
    // Implementation code here...
}
export default my_singleton;

You then can import this module into other modules and still always have the same object. This works because bundlers and browsers are smart enough to avoid the repetition of imports, so once this object has been brought in the first time, it will ignore the next requests. When not using a bundler, the ES6 implementation of JavaScript also defines that modules are singletons.

Method 2

This method creates a class and then, on the first instantiation, saves the reference for future calls. In order for this to work, we use a variable (traditionally called _instance) from the class and save the reference to the instance in the constructor. In the following calls, we check whether the _instance value exists, and if so, return it. Here is the code:

./chapter 2/singleton-class.js

class myClass{
    constructor(){
        if(myClass._instance){
            return myClass._instance;
        }else{
            myClass._instance=this;
        }
        return this;
    }
}
export default new myClass()

This second method may be more familiar to other language developers. Notice how we are also exporting a new instance of the class and not the class directly. This way, the invoker will not have to remember to instantiate the class every time, and the code will be the same as in method 1. This use case is something that needs to be coordinated with your team to avoid different implementations.

The invoker then can call methods from each one directly (assuming the singleton has a function/method called myFunction()):

./chapter 2/singleton-invoker.js

import my_method1_singleton from "./singleton-json";
import my_method2_singleton from "./singleton-class";
console.log("Look mom, no instantiation in both cases!")
my_method1_singleton.myFunction()
my_method2_singleton.myFunction()

The singleton pattern is extremely useful, though it rarely exists in isolation. Often, we use singletons to wrap the implementation of other patterns and make sure we have a single point of access. In our examples, we will use this pattern quite often.

The dependency injection pattern

This pattern simply states that the dependencies for a class or function are provided as inputs, for example, as parameters, properties, or other types of implementations. This simple statement opens a very wide range of possibilities. Let’s take, for example, a class that works with the browser’s IndexedDB API through an abstraction class. We will learn more about the IndexedDB API in Chapter 7, Data Flow Management, but for now, just concentrate on the dependency part. Consider that the dbManager.js file exposes an object that handles the operations with the database, and the projects object deals with CRUD operations for the projects table (or collection). Without using dependency injection, you will have something like this:

./chapter 2/dependency-injection-1.js

import dbManager from "dbManager"
const projects={
    getAllProjects(){
        return dbManager.getAll("projects")
    }
}
export default projects;

The preceding code shows a “normal” approach, where we import the dependencies at the beginning of the file and then use them in our code. Now, let’s tweak this same code to use dependency injection:

./chapter 2/dependency-injection-2.js

const projects={
    getAllProjects(dbManager){
        return dbManager.getAll("projects")
    }
}
export default projects;

As you can see, the main difference is that dbManager is now passed as a parameter to the function. This is what is called injection. This opens up many ways to manage dependencies and, at the same time, pushes the hardcoding of dependencies up the implementation tree. This makes this class highly reusable, at least for as long as the dependency respects the expected API.

The preceding example is not the only way to inject a dependency. We could, for example, assign it to a property for the object’s internal use. For example, if the projects.js file was implemented using the property approach instead, it would look like this:

./chapter 2/dependency-injection-3.js

const projects={
    dbManager,
    getAllProjects(){
        return this.dbManager.getAll("projects")
    }
}
export default projects;

In this case, the invoker of the object (a singleton, by the way) needs to be aware of the property and assign it before calling on any of its functions. Here is an example of how that would look:

./chapter 2/dependency-injection-4.js

import projects from "projects.js"
import dbManager from "dbManager.js"
projects.dbManager=dbManager;
projects.getAllProjects();

But this approach is not recommended. You can clearly see that it breaks the principle of encapsulation, as we are directly assigning a property for the object. It also doesn’t feel like clean code even though it is valid code.

Passing the dependencies one function at a time is also not recommended. So, what is a better approach? It depends on the implementation:

  • In a class, it is convenient to require the dependencies in the constructor (and if not found, throw an error)
  • In a plain JSON object, it is convenient to provide a function to set the dependency explicitly and let the object decide how to use it internally

This last approach is also recommended for passing a dependency after the instantiation of an object when the dependency is not ready at the time of implementation

Here is a code example for the first point mentioned in the preceding list:

./chapter 2/dependency-injection-5.js

class Projects {
    constructor(dbManager=null){
        if(!dbManager){
            throw "Dependency missing"
        }else{
            this.dbManager=dbManager;
        }
    }
}

In the constructor, we declare the expected parameter with a default value. If the dependency is not provided, we throw an error. Otherwise, we assign it to an internal private attribute for the use of the instance. In this case, the invoker should look like this:

// Projects are a class
import Projects from "projects.js"
import dbManager from "dbManager.js"
try{
    const projects=new Projects(dbManager);
}catch{
    // Error handler here
}

In an alternative implementation, we could have a function that basically does the same by receiving the dependency and assigning it to a private attribute:

import projects from "projects.js"
import dbManager from "dbManager.js"
projects.setDBManager(dbManager);

This approach is better than directly assigning the internal attribute, but you still need to remember to do the assignment before using any of the methods in the object.

Best practice note

Whatever approach you use for dependency injection, remain constant throughout your code base.

You may have noticed that we have mainly been focusing on objects. As you may have already guessed, passing a dependency to a function is just the same as passing another parameter, so it does not deserve special attention.

This example has just moved the dependency implementation responsibility up to another class in the hierarchy. But what if we implement a singleton pattern to handle all or most of the dependencies in our application? This way, we could just delegate the loading of the dependencies to one class or object at a determined point in our application life cycle. But how do we implement such a thing? We will need the following:

  • A method to register the dependency
  • A method to retrieve the dependency by name
  • A structure to keep the reference to each dependency

Let’s put that into action and create a very naive implementation of such a singleton. Please keep in mind that this is an academic exercise, so we are not considering error checking, de-registration, or other considerations:

./chapter 2/dependency-injection-6.js

const dependencyService={                          //1
    dependencies:{},                               //2
    provide(name, dependency){                     //3
        this.dependencies[name]=dependency         //4
        return this;                               //5
    },
    inject(name){                                  //6
        return this.dependencies[name]??null;      //7
    }
}
export default dependencyService;

With this bare minimum implementation, let’s look at each line by the line comment:

  1. We create a simple JavaScript object literal as a singleton.
  2. We declare an empty object to use as a dictionary to hold our dependencies by name.
  3. The provide function lets us register a dependency by name.
  4. Here, we just use the name as the field name and assign the dependency passed by argument (notice we are not checking pre-existing names, etc.).
  5. Here, we return the source object, mainly for convenience so we can chain the invocation.
  6. The inject function will take the name as registered in the provide function.
  7. We return the dependency or null if not found.

With that singleton on board, we can now use it across our application to distribute the dependencies as needed. For that, we need a parent object to import them and populate the service. Here is an example of how that might look:

./chapter 2/dependency-injection-7.js

import dependencyService from "./dependency-injection-6"
import myDependency1 from "myFile1"
import myDependency2 from "myFile2"
import dbManager from "dbManager"
dependencyService
    .provide("dependency1", myDependency1)
    .provide("dependency2", myDependency2)
    .provide("dbManager", dbManager)

As you can see, this module has hard-coded dependencies, and its work is to load them into the dependencyService object. Then, the dependent function or object needs only to import the service and retrieve the dependency it needs by the registration name like this:

import dependencyService from "./dependency-injection-6"
const dbManager=dependencyService.inject("dbManager")

This approach does create a tight coupling between components but is here as a reference. It has the advantage that we can control all the dependencies in a single location so that the maintenance benefits could be significant. The choice of names for the methods of the dependencyService object was not random either: these are the same used by Vue 3 inside the component’s hierarchy. This is very useful for implementing some User Interface design patterns. We will see this in more detail in Chapter 4, User Interface Composition with Components and Chapter 7, Data Flow Management.

As you can see, this pattern is very important and is implemented in Vue 3 with the provide/inject functions. It's a great addition to our toolset, but there is more still. Let’s move on to the next one.

The factory pattern

The factory pattern provides us with a way to create objects without creating a direct dependency. It works through a function that, based on the input, will return an instantiated object. The use of such an implementation will be made through a common or standard interface. For example, consider two classes: Circle and Square. Both implement the same draw() method, which draws the figure to a canvas. Then, a factory function would work something like this:

function createShape(type){
    switch(type){
        case "circle": return new Circle();
        case "square": return new Square();
}}
let
    shape1=createShape("circle"),
    shape2=createShape("square");
shape1.draw();
shape2.draw();

This method is quite popular, especially in conjunction with other patterns, as we will see multiple times in this book.

The observer pattern

The observer pattern is very useful and one of the basis of a reactive framework. It defines a relationship between objects where one is being observed (the subject) for changes or events, and other(s) are notified of such changes (the observers). The observers are also called listeners. Here is a graphical representation:

Figure 2.3 – The subject emits an event and notifies the observers

Figure 2.3 – The subject emits an event and notifies the observers

As you can see, the subject emits an event to notify the observers. It is for the subject to define what events and parameters it will publish. Meanwhile, the observers subscribe to each event by registering a function with the publisher. This implementation is why this pattern is often referred to as the pub/sub pattern, and it can have several variations.

When looking into the implementation of this pattern, it is important to notice the cardinality of the publication: 1 event to 0..N observers (functions). This means that the subject must implement, on top of its main purpose, the functionality to publish events and keep track of the subscribers. Since this would break a principle or two in the design (separation of concerns, single responsibility, etc.), it is common to extract this functionality into a middle object. The previous design then changes to add a middle layer:

Figure 2.4 – An observer implementation with a dispatcher middle object

Figure 2.4 – An observer implementation with a dispatcher middle object

This middle object, sometimes referred to as an “event dispatcher encapsulates the basic functionality to register observers, receive events from the subject, and dispatch them to the observers. It also does some clean-up activities when an observer is no longer observing Let’s put these concepts into a simple and naive implementation of an event dispatcher in plain JavaScript:

./chapter 2/Observer-1.js

class ObserverPattern{
constructor(){
    this.events={}                                             //1
}
on(event_name, fn=()=>{}){                                     //2
    if(!this.events[event_name]){
       this.events[event_name]=[]
    }
    this.events[event_name].push(fn)                           //3
}
emit(event_name, data){                                        //4
    if(!this.events[event_name]){
       return
    }
for(let i=0, l=this.events[event_name].length; i<l; i++){
    this.events[event_name][i](data)
}
}
off(event_name, fn){                                           //5
    let i=this.events[event_name].indexOf(fn);
    if(i>-1){
        this.events[event_name].splice(i, 1);
    }
}
}

The preceding implementation is, again, naive. It doesn’t contain the necessary error and edge case handling that you would use in production, but it does have the bare basics for an event dispatcher. Let’s look into it line by line:

  1. In the constructor, we declare an object to use internally as a dictionary for our events.
  2. The on method allows the observers to register their functions. In this line, if the event is not initialized, we create an empty array.
  3. In this line, we just push the function to the array (as I said, this is a naive implementation, as we don’t check for duplicates, for example).
  4. The emit method allows the subject to publish an event by its name and pass some data to it. Here, we run over the array and execute each function passing the data we received as a parameter.
  5. The off method is necessary to deregister the function once it is not used (see the keep it clean principle, earlier in this chapter).

In order for this implementation to work, every observer and the subject need to reference the same implementation of the ObserverClass. The easiest way to secure this is to implement it through a singleton pattern. Once imported, each observer registers with the dispatcher with this line:

import dispatcher from "ObserverClass.js"    //a singleton
dispatcher.on("event_name", myFunction)

Then, the subject emits the event and passes the data with the following lines:

import dispatcher from "ObserverClass.js"    //a singleton
dispatcher.emit("event_name", data)

Finally, when the observer no longer needs to watch the subject, it needs to clean up the reference with the off method:

dispatcher.off("event_name", myFunction)

There are a good number of edge cases and controls that we have not covered here, and rather than reinventing the wheel, I suggest using a ready-made solution for these cases. In our book, we will use one named mitt (https://www.npmjs.com/package/mitt). That has the same methods as in our example. We will see how to install packaged dependencies in Chapter 3, Setting up a Working Project.

The command pattern

This pattern is very useful and easy to understand and implement. Instead of executing a function right away, the basic concept is to create an object or structure with the information necessary for the execution. This data package (the command) is then delegated to another object that will perform the execution according to some logic to handle it. For example, the commands can be serialized and queued, scheduled, reversed, grouped together, and transformed. Here is a graphical representation of this pattern with the necessary parts:

Figure 2.5 – A graphical implementation of the command pattern

Figure 2.5 – A graphical implementation of the command pattern

The diagram shows how the clients submit their commands to the Invoker. The invoker usually implements some sort of queue or task array to handle the commands and then routes the execution to the proper Receiver. If there is any data to return, it also returns it to the proper client. It is also common that the invoker attaches additional data to the command to keep track of clients and receives, especially in the case of asynchronous executions. It also provides a single point of “entry” to the receivers and decouples the “clients” from them.

Let’s again work on a naive implementation of an Invoker class:

./chapter 2/Command-1.js

class CommandInvoker{
    addCommand(command_data){                          //1
        // .. queue implementation here
    }
    runCommand(command_data){                          //2
        switch(command_data.action){                   //3
            case "eat":
                // .. invoke the receiver here
                break;
            case "code":
                // .. invoke the receiver here
                break;
            case "repeat":
                // .. invoke the receiver here
                break;
        }
    }
}

In the preceding code, we have implemented a bare-bones example of what an Invoker should have line by line:

  1. The Invoker exposes a method to add commands to the object. This is only necessary when the commands will be somehow queued, serialized, or processed according to some logic.
  2. This line executes the command according to the action field contained in the command_data parameter.
  3. Based on the action field, the invoker routes the execution to the proper receiver.

There are many ways to implement the logic for routing the execution. It is important to notice that this pattern can be implemented on a larger scale depending on the context. For example, the invoker might not even be in the web client application and be on the server or on a different machine. We will see an implementation of this pattern in Chapter 8, Multithreading with Web Workers, where we use this pattern to process tasks between different threads and unload the main thread (where Vue 3 runs).

The proxy pattern

The definition for this pattern comes directly from its name, as the word “proxy” means something or someone who acts on behalf of another as if it was the same. That is a mouthful, but it will make you remember it. Let’s look into an example to clarify how this works. We will need at least three entities (components, objects, etc.):

  • A client entity that needs to access the API of a target entity
  • A target entity that exposes a well-known API
  • A proxy object that sits in between and exposes the same API as the target while at the same time intercepting every communication from the client and relaying it to the target

We can graphically represent the relationship between these entities in this way:

Figure 2.6 – The proxy object exposes the same API as the target

Figure 2.6 – The proxy object exposes the same API as the target

The key factor for this pattern is that the proxy behaves and exposes the same API as the target, in such a way that the client does not know or doesn’t need to know that it is dealing with a proxy and not the target object directly. So, why would we want to do such a thing? There are many good reasons, such as the following:

  • You need to maintain the original unmodified API, but at the same time:
    • Need to process the inputs or outputs for the client
    • Need to intercept each API call to add internal functionality, such as maintenance operations, performance improvements, error checking, and validation
    • The target is an expensive resource, so a proxy could implement logic to leverage their operations (for example, a cache)
  • You need to change the client or the target but can’t modify the API
  • You need to maintain backward compatibility

There are more reasons that you may come across, but I hope that by now you can see how this can be useful. Being a pattern, this template can be implemented on multiple levels, from a simple object proxy to a full application or server. It is quite common when performing partial upgrades of a system or application. On a lower level, JavaScript even natively includes a constructor for proxying objects that Vue 3 uses internally to create reactivity.

In Chapter 1, The Vue 3 Framework, we reviewed the options for reactivity with the ref() but this new version of Vue also includes another alternative for complex structures, called reactive(). The first one uses pub/sub methods (the observer pattern!), but the latter uses native proxy handlers (this pattern!). Let’s look into an example of how this native implementation may work with a naive partial implementation.

In this simple example, we will make an object with reactive properties automatically convert Celsius degrees to and back from Fahrenheit using a Proxy object:

./chapter 2/proxy-1.js

let temperature={celsius:0,fahrenheit: 32},                    //1
    handler={                                                  //2
      set(target, key, value){                                 //3
         target[key]=value;                                    //4
    switch(key){
     case "celsius":
           target.fahrenheit=calculateFahrenheit(value);       //5
           break;
    case "fahrenheit":
           target.celsius=calculateCelsius(value);
         }
      },
      get(target, key){
         return target[key];                                   //6
      }
    },
    degrees=new Proxy(temperature, handler)                    //7
// Auxiliar functions
function calculateCelsius(fahrenheit){
    return (fahrenheit - 32) / 1.8
}
function calculateFahrenheit(celsius){
    return (celsius * 1.8) + 32
}
degrees.celsius=25                                             //8
console.log(degrees)
// Prints in the console:
// {celsius:25, fahrenheit:77}                                 //9

Let’s review the code line by line to see how this works:

  1. In this line, we declare the temperature object, which is going to be our target to be proxied. We initialize its two properties with an equal converted value.
  2. We declare a handler object, which will be our proxy for the temperature object.
  3. The set function in the proxy handler receives three arguments: the target object, the key referred to, and the value attempted to be assigned. Notice that I say “attempted”, as the operation has been intercepted by the proxy.
  4. On this line, we perform the assignment as intended to the object property. Here, we could have done other transformations or logic, such as validation or raised an event (the observer pattern again!).
  5. Notice how we use a switch to filter the property names that we are interested in. When the key is celsius, we calculate and assign the value in Fahrenheit. The opposite happens when we receive an assignment for fahrenheit degrees. This is where the reactivity comes into play.
  6. For the get function, at least in this example, we just specifically return the value requested. In the way this is implemented, it would be the same as if we skip the getter function. However, it is here as an example that we could operate and transform the value to be returned as this operation is also intercepted.
  7. Finally, in line 7, we declare the degrees object as the proxy for temperature with the handler.
  8. On this line, we test the reactivity by assigning a value in Celsius to the member of the degrees object, just like we normally would to any other object.
  9. When we print the degrees object to the console, we notice that the fahrenheit property has been automatically updated.

This is a rather limited and simple example of how the native Proxy() constructor works and applies the pattern. Vue 3 has a more complex approach to reactivity and tracking dependencies, using the proxy and observer patterns. However, this gives us a good idea of what approach is happening behind the scenes when we see the HTML updated live in front of our very eyes.

The concept of proxying between a client and a target is also related to the next two patterns: the decorator and the façade patterns since they are also a sort of proxy implementation. The distinguishing key factor is that the proxy retains the same API as the original target object.

The decorator pattern

This pattern may, at first sight, seem very similar to the proxy pattern, and indeed it is, but it adds a few distinctive features that set it apart. It does have the same moving parts as the proxy, meaning there is a Client, a Target, and a Decorator in between that implements the same interface as the target (yes, just like in the proxy pattern). However, while in the Proxy pattern the intercepted API calls mainly deal with the data and internal maintenance (“housekeeping”), the decorator augments the functionality of the original object to do more. This is the defining factor that separates them.

In the proxy example, notice how the additional functionality was an internal reactivity to keep the degrees in each scale synchronized. When you change one, it internally and automatically updates the other. In a decorator pattern, the proxy object performs additional operations before, during, or after executing the API call to the target object. Just like in the proxy pattern, all of this is transparent for the client object.

For example, building on the previous code, imagine that now we want to log each call to the API of a certain target while keeping the same functionality. Graphically, it would look like this:

Figure 2.7 – An example of a decorator that augments the target with a logging feature

Figure 2.7 – An example of a decorator that augments the target with a logging feature

Here, what was first a simple proxy, now by the mere act of performing a humble logging call, has now become a decorator. In the code, we only need to add this line before the end of the set() method (assuming there is also a function named getTimeStamp()):

console.log(getTimeStamp());

Of course, this is a simple example just to make a point. In the real world, decorators are very useful for adding functionality to your application without having to rewrite the logic or significant portions of your code. On top of this, decorators can be stackable or chainable, meaning that you can create “decorators for decorators” if needed, so each one will represent one step of added functionality that would maintain the same API of the target object. And just like that, we are beginning to step into the boundaries of a middleware pattern, but we will not cover it in this book. Anyway, the idea behind that other pattern is to create layers of middleware functions with a specified API, each one that performs one action, but with the difference that any step can decide to abort the operation, so the target may or may not be called. But that is another story... let’s get back to decorators.

Previously in this book, we mentioned that Vue 3 components do not have inheritance like plain JavaScript classes implemented by extending from one another. Instead, we can use the decorator pattern on components to add functionality or change the visual appearance. Let’s look at a brief example now, as we will see components and UI design in detail in Chapter 4, User Interface Composition with Components.

Consider that we have the simplest of components that displays a humble h1 tag with a title that receives the following as input:

./chapter 2/decorator-1.vue

<script setup>
    const $props=defineProps(['label'])          //1
</script>
<template>
    <h1>{{$props.label}}</h1>                    //2
</template>
<style scoped></style>

In this simple component, we declare a single input named label in line //1. Don’t worry about the syntax for now, as we will see this in detail in Chapter 4, User Interface Composition with Components. On line //2, we are interpolating the value plainly inside the h1 tags just as expected.

So, to create a decorator for this component we need to apply the following simple rules:

  • It has to act on behalf of the component (object)
  • It has to respect the same API (inputs, outputs, function calls, etc.)
  • It has to augment the functionality or visual representation before, after, or during the execution of the target API

With that in mind, we can create a decorator component that intercepts the label attribute, changes it a bit, and also modifies the visual appearance of the target component:

./chapter 2/decorator-2.vue

<script setup>
    import HeaderH1 from "./decorator-1.vue"
    const $props=defineProps(['label'])                //1
</script>
<template>
    <div style="color: purple !important;">            //2
        <HeaderH1 :title="$props.label+'!!!'">         //3
        </HeaderH1>
    </div>
</template>

In this code, in line //1, you can see that we keep the same interface as the target component (that we imported in the previous line), and then in line //2, we modify (augment) the color attribute and in line //3 we are also modifying the data passed to the target component by adding three exclamation marks. With those simple tasks, we have kept the conditions to build a decorator pattern extrapolated to Vue 3 components. Not bad at all.

Decorators are very useful, but there is still one more proxy-like pattern that is also very common and handy: the façade pattern.

The façade pattern

By now, you may have seen the progressive pattern in these, well, patterns. We started with a proxy to act on behalf of another object or entity, we augmented it with the use of decorators while keeping the same API, and now is the turn for the façade pattern. Its job is, in addition to the functions of a proxy and decorator, to simplify the API and hide the large complexity behind it. So, a façade sits between a client and a target, but now the target is highly complex, being an object or even a system or multiple subsystems. This pattern is also used to change the API of an object or to limit the exposure to the client. We can picture the interactions as follows:

Figure 2.8 – A façade object simplifying the interaction with a complex API or system

Figure 2.8 – A façade object simplifying the interaction with a complex API or system

As you can see, the main purpose of the façade is to offer a simpler approach to a complex interaction or API. We will use this pattern many times during our examples to simplify native implementations in the browser with more developer-friendly approaches. We will use libraries to encapsulate the use of IndexedDB and create our own simplified communication with web workers in Chapter 8, Multithreading with Web Workers.

Needless to say, you will have seen this pattern in action before, as it is one of the foundational concepts of modern technology. Hiding complexity behind a simple interface (API) is all around us and is a big part of web development. After all, the entire internet is extremely complicated, with thousands of moving parts, and the technology that makes up web pages is close to magic. Without this pattern, we would still be programming with zeros and ones.

In practice, you will add layers of simplification to your own applications to break down complexity. One way to do it is to use third-party libraries that provide a simplified interface. In the following chapters, we will use some of these, such as the following:

  • Axios: To handle all Asynchronous JavaScript and XML (AJAX) communications with the server
  • DexieDB: To handle the API to IndexedDB (the browser’s local database)
  • Mitt: To create event pipelines (we mentioned this in the Observer pattern)
  • Vue 3: To create amazing UIs

In general, there are façade libraries for most of the native implementations of web technologies, which are well battle tested. Developers are very good at simplifying these and sharing the code with others, thanks to the open source movement. Still, when using other people’s modules, make sure they are “safe.” Don’t reinvent the wheel, and don’t repeat yourself But now, it is time to move on to the next pattern in our list.

The callback pattern

The callback pattern is easy to understand. It applies when an operation needs to be executed after a synchronous or asynchronous operation has finished. For this, the function invocation includes, as one of the parameters, a function to be executed when the operations are completed. Having said that, we need to distinguish between the following two types of code flow:

  • Synchronous operations are executed one after another in sequential order. It is the basic code flow, top to bottom.
  • Asynchronous operations are executed out of the normal flow once invoked. Their length is uncertain, as well as their success or failure.

It is for asynchronous cases that the callback pattern is especially useful. Think, for example, of a network call. Once invoked, we don’t know how long it will take to get an answer from the server and whether it will succeed, fail, or throw an error. If we didn’t have asynchronous operations, our application would be frozen, waiting until a resolution happens. That would not be a good user experience, even though it would be computationally correct.

One important feature in JavaScript is that, being single-threaded, asynchronous functions don’t block the main thread allowing the execution to continue. This is important since the rendering functions of the browser run on the same thread. However, this is not free as they do consume resources, but they won’t freeze the UI, at least in theory. In practice, it will depend on a number of factors heavily influenced by the browser environment and the hardware. Still, let’s stick to the theory.

Let’s see an example of a synchronous callback function and turn it asynchronous. The example function is very simple: we will calculate the Fibonacci value of a given number using the callback pattern. But first, a refresher on the formula for the calculation:

F(0)=0
F(1)=1
F(n)=F(n-1)+F(n-2), with n>=2

So, here is a JavaScript function that applies the formula and receives a callback to return the value. Notice that this function is synchronous:

./chapter 2/callback-1.js - Synchronous Fibonacci

function FibonacciSync(n, callback){
    if(n<2){
       callback(n)
    } else{
        let pre_1=0,pre_2=1,value;
        for(let i=1; i<n; i++){
           value=pre_1+pre_2;
           pre_1=pre_2;
           pre_2=value;
        }
        callback(value)
    }
}

Notice how instead of returning the value with return, we are passing it as a parameter to the callback function. When is it useful to use such a thing? Consider these simple examples:

FibonacciSync(8, console.log);
// Will print 21 to the console
FibonacciSync(8, alert)
// Will show a modal with the number 21

Just by replacing the callback function, we can considerably alter how the result is presented. However, the example function has a fundamental flaw affecting the user experience. Being synchronous, the calculation time is proportional to the parameter passed: the larger n, the more time it will take. With a sufficiently large number, we can easily hang up the browser, but also, much before that, we can freeze the interface. You can test that the execution is synchronous with the following snippet:

console.log("Before")
FibonacciSync(9, console.log)
console.log("After")
// Will output
// Before
// 34
// After

To turn this simple function into an asynchronous function, you can simply wrap the logic inside a setImmediate call. This will take the execution out of the normal workflow. The new function now looks like this:

function FibonacciAsync(n, callback){
    setImmediate(()=>{
        if (n<2){
            callback(n)
        } else{
            let pre_1=0,pre_2=1,value;
            for(let i=1; i<n; i++){
                value=pre_1+pre_2;
                pre_1=pre_2;
                pre_2=value;
            }
            callback(value);
        }
    })
}

As you can see, we use an arrow function to wrap up the code without any modifications. Now, see the difference when we execute the same snippet as before with this function:

console.log("Before")
FibonacciAsync(9, console.log)
console.log("After")
// Will output
// Before
// After
// 34

As you can see by the output, the snippet outputs After before 34. This is because our asynchronous operation has been taken out of the normal flow as expected. When calling an asynchronous function, the execution does not wait for a result and continues executing the next instruction. This can be confusing at times but is very powerful and useful. However, the pattern does not prescribe how to handle errors or failed operations or how to chain or sequentially run multiple calls. There are different ways to deal with those cases, but they are not part of the pattern. There is another way to handle asynchronous operations that offers more flexibility and control: promises. We will see this next, and in most cases, you can use either pattern interchangeably. I say, “in most cases,” not all!

The promise pattern

The promises pattern is made primarily to deal with asynchronous operations. Just like with callbacks, the invocation of a promised function takes the execution out of the normal flow, but it returns a special object called Promise. This object exposes a simple API with three methods: then, catch, and finally:

  • The then method receives two callback functions, traditionally called resolve and reject. They are used in the asynchronous code to return a successful value (resolve) or a failed or negative value (reject).
  • The catch method receives an error parameter and is triggered when the process throws an error and the execution is interrupted.
  • The finally method executes in either case and receives a callback function.

While a promise is running, it is said to be in an indeterminate state until it is resolved or rejected. There is no time limit for how long a promise will wait in this state, something that makes it especially useful for lengthy operations such as network calls and inter-process communication (IPC).

Let’s see how to implement the previous example with the Fibonacci series using promises:

function FibonacciPromise(n) {
    return new Promise((resolve, reject) => {          //1
        if (n < 0) {
            reject()                                   //2
        } else {
             if (n < 2) {
                 resolve(n)                            //3
             } else {
                  let pre_1 = 1, pre_2 = 1, value;
                  for (let i = 2; i < n; i++) {
                      value = pre_1 + pre_2;
                      pre_1 = pre_2;
                      pre_2 = value;
                  }
                  resolve(value);
             }
        }
    })
}

At first sight, it is easy to see that the implementation has changed a bit. We start on line //1 by immediately returning a new Promise() object. This constructor receives a callback function, that will, in turn, receive two callbacks named resolve() and reject(). We need to use these in our logic to return a value in case of success (resolve) or failure (reject). Also notice that we don’t have to wrap our code in a setImmediate function, as a promise is by nature asynchronous. We now check for negative numbers and then reject the operation in that case (line //2). The other change we make is to replace the callback() invocation for resolve() in lines//3 and //4.

The invocation now also changes:

console.log("Before")
FibonacciPromise(9).then(
    value=>console.log(value),
    ()=>{console.log("Undefined for negative numbers!")}
);
console.log("After")
// Will output:
// Before
// After
// 34

As you can see, we chain to the invocation, the then method, and pass to it the two functions for success and failure (resolve and reject in our code). Just like before, we get the same output. Now, this may seem more verbose (it is), but the benefits greatly outweigh the extra typing. Promises are chainable, meaning that for successful operations, you can return a new promise and, that way, have a sequential operation. Here is an example:

MyFunction()
    .then(()=>{ return new Promise(...)}, ()=>{...})
    .then(()=>{ return new Promise(...)}, ()=>{...})
    .then(()=>{ return new Promise(...)}, ()=>{...})
    .then(()=>{ return new Promise(...)}, ()=>{...})
    .catch(err=>{...})

There are other methods exposed by the Promise constructor, such as .all, but I will refer you to the documentation to dig deeper into the possibilities and syntax (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). Still, quite verbose. Lucky for us, JavaScript provides us with a simplified syntax to handle promises, async/await, and think of them as a way to code in a more “traditional” way. This only applies to the invocation of promised functions and can only be used in functions.

To see this as an example, let’s imagine that we have three functions that return promises, named MyFuncA, MyFuncB, and MyFuncC (yes, I know, not the greatest names). Each one returns, in case of success, one single value (this is a condition). These are then used within MyProcessFunction with the new syntax. Here is the declaration:

async function myProcessFunction() {                  //1
    try {                                             //2
         let     a = await MyFuncA(),                 //3
                 b = await MyFuncB(),
                 c = await MyFuncC()
         console.log(a + b + c)                       //4
    } catch {
             console.log("Error")
    }
}
// Invoke the function normally
MyProcessFunction()                                   //5

We start by declaring our function with the async keyword (line //1). This signals to the interpreter that we will use the await syntax inside our function. One condition is to wrap the code in a try...catch block. Then, we can use the await keyword in front of the invocation of each promised function call, as in line //3. By line //4, we are certain that each variable has received a value. Certainly, this approach is easier to follow and read.

Let’s investigate the equivalences for the line:

let a=await MyFuncA()

This will match the thenable (using .then) syntax:

let a;
MyFuncA()
    .then(result=>{ a=result; })

However, the problem with this last syntax is that we need to make sure that all the variables a, b, and c have values before we can run line //4, console.log(a+b+c), which would mean chaining the invocations like this:

let a,b,c;
MyFuncA()
    .then(result=>{ a=result; return MyFuncB()})
    .then(result=>{ b=result; return MyFuncC()})
    .then(result=>{ c=result; console.log(a+b+c)})

This format is harder to follow and certainly more verbose. For these cases, the async/await syntax is preferred.

The use of promises is great for wrapping lengthy or uncertain operations and integrating with other patterns that we have seen (façade, decorator, etc.). It is an important pattern to keep in mind that we will use extensively in our applications.

You have been reading a chapter from
Vue.js 3 Design Patterns and Best Practices
Published in: May 2023
Publisher: Packt
ISBN-13: 9781803238074
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Banner background image