Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Vue.js 3 Design Patterns and Best Practices
Vue.js 3 Design Patterns and Best Practices

Vue.js 3 Design Patterns and Best Practices: Develop scalable and robust applications with Vite, Pinia, and Vue Router

Arrow left icon
Profile Icon Pablo David Garaguso
Arrow right icon
₹800 per month
Full star icon Full star icon Full star icon Full star icon Half star icon 4.8 (10 Ratings)
Paperback May 2023 296 pages 1st Edition
eBook
₹799.99 ₹2382.99
Paperback
₹2978.99
Subscription
Free Trial
Renews at ₹800p/m
Arrow left icon
Profile Icon Pablo David Garaguso
Arrow right icon
₹800 per month
Full star icon Full star icon Full star icon Full star icon Half star icon 4.8 (10 Ratings)
Paperback May 2023 296 pages 1st Edition
eBook
₹799.99 ₹2382.99
Paperback
₹2978.99
Subscription
Free Trial
Renews at ₹800p/m
eBook
₹799.99 ₹2382.99
Paperback
₹2978.99
Subscription
Free Trial
Renews at ₹800p/m

What do you get with a Packt Subscription?

Free for first 7 days. ₹800 p/m after that. Cancel any time!
Product feature icon Unlimited ad-free access to the largest independent learning library in tech. Access this title and thousands more!
Product feature icon 50+ new titles added per month, including many first-to-market concepts and exclusive early access to books as they are being written.
Product feature icon Innovative learning tools, including AI book assistants, code context explainers, and text-to-speech.
Product feature icon Thousands of reference materials covering every tech concept you need to stay up to date.
Subscribe now
View plans & pricing
Table of content icon View table of contents Preview book icon Preview Book

Vue.js 3 Design Patterns and Best Practices

Software Design Principles and Patterns

Software development is fundamentally a human-intensive discipline. This means that it requires knowledge of both techniques and technology, but also comprehension of the problem and the ability to make decisions to implement a solution on multiple levels of abstraction. Programming has much to do with how a developer thinks. Over the years, and within each context and language, guidelines and solutions have emerged to solve recurring problems. Knowledge of these patterns will help you identify when to apply them and speed your development on a sure footing. On the other hand, principles are guiding concepts that should be applied at every stage of the process and have more to do with how you approach the process.

In this chapter, we will take a look at a non-exclusive and non-exhaustive list of principles and patterns that are common in Vue 3 application development.

Principles

Patterns

  • Separation of concerns
  • Composition over inheritance
  • Single responsibility
  • Encapsulation
  • KIC – keep it clean
  • DRY – don’t repeat yourself
  • KISS – keep it simple stupid
  • Code for the next
  • Singleton
  • Dependency injection
  • Observer
  • Command
  • Proxy
  • Decorator
  • Façade
  • Callbacks
  • Promises

Table 2.1 The principles and patterns covered in this chapter

Understanding these principles and patterns will help you use the framework more efficiently and more often than not, it will prevent you from “reinventing the wheel”. Together with the first chapter, this will conclude the foundational part of this book and will give you the basis to follow the practical parts and implementation of application examples in the rest of the book.

What are the principles of software design?

In software development, design principles are high-level conceptual guidelines that should apply to the entire process. Not every project will use the same principles, and these are not mandatory rules to be enforced. They can appear in a project from the architecture down to the user interface (UI) and the last bit of code. In practice, some of these principles can also influence software attributes, such as maintainability and re-usability.

A non-exclusive list of design principles

Design principles vary somewhat depending on the context, domain, and even the team one may be part of at the time. The principles included in this chapter are, therefore, non-exclusive.

Separation of concerns

This is perhaps the most important principle in software engineering. Separation of concerns implies that a system must be divided into subsystems of elements grouped by their function or service (the concern). For example, we can consider the human body as a system composed of many subsystems (respiratory, circulatory, digestive, etc.). These, in turn, are integrated by different organs, which are made of tissues, and so forth, down to the smallest cell. Following the same idea in software, an application can be divided into elements grouped by concerns, from the large architecture all the way down to the last function. Without this breakdown of complexity into manageable parts, creating a functional system would be much harder, if not impossible.

In general, the application of this principle starts with the big picture of what the system should be, looks into what it should do to accomplish that, and then breaks it down into manageable working parts.

As an example, here is a crude graphical representation of separation of concerns for a web application. Each box in this diagram identifies a different concern that, in turn, can be detailed into smaller functional parts. Even better, you can see how this principle allows you to identify the integrating parts of a system.

Figure 2.1 – A simple architectural view of a web application showing separation of concerns

Figure 2.1 – A simple architectural view of a web application showing separation of concerns

If we were to drill down into any of these small boxes within their respective domains, we could still find more concerns to subdivide until we reach an indivisible atomic element (a component or function, for example). This principle has much to do with and benefits from other principles, such as abstraction and single responsibility. We will review them further down the line in this same chapter.

Composition over inheritance

The principle of composition over inheritance comes directly from Object-Oriented Programming (OOP). It states that an object should attempt to use other objects’ functionality when needed, by referencing or instantiating them instead of creating a large and complex inheritance family tree of classes to add such functionality. Now, JavaScript is fundamentally a functional language, even though it supports multiple paradigms, including features from OOP, so this principle applies as well. There is one note of warning for those migrating from OOP into JavaScript, and that is to avoid the temptation to treat JavaScript as a pure OOP language. Doing so could create unnecessary complexity instead of benefiting from the virtues of the language.

In Vue 3, there is no extension or inheritance of components. When we need shared or inherited functionality, we have a nice toolset of options to replace the inheritance paradigm. We will see later how we can comply with this principle by using composable components in Chapter 4, User Interface Composition with Components.

Single responsibility principle

This principle can be found in OOP as well as in functional programming. Simply put, it states that a class, method, function, or component should deal with only one responsibility or functionality. If you have worked in other disciplines and languages, this comes naturally. Multipurpose functions are hard to maintain and tend to grow out of control, especially in a language such as JavaScript, which is loosely typed and highly dynamic. The same concept also applies directly to Vue 3 components. Each component should deal with one specific operation and avoid attempting to do too much by itself. In practice, when a component grows beyond a certain scope, it is best to split it into multiple components or extract the behavior into external modules. There are cases when you may end up with a many-thousand-lines-long component, but in my experience, this is rarely necessary and can and should be avoided. A warning, though, is that too much specificity could also lead to unnecessary complexity.

As an example, let’s imagine a sign-in screen that also displays a sign-up option. This approach is common on many sites today. You could include all the functionalities inside just one component, but that would break this principle. A better alternative would be to split the components into at least three components for this task:

  • A parent component that handles the UI logic. This component decides when to show/hide the sign-in and sign-up components.
  • A child component that handles the sign-in function.
  • A child component that handles the sign-up function.

Here is a graphical representation of this configuration:

Figure 2.2 – The composition of a sign-in/up interface using multiple components

Figure 2.2 – The composition of a sign-in/up interface using multiple components

I think that you can quickly grasp the benefits of this principle. It makes the code easy to manage, maintain, and adapt since web applications have the tendency to mutate and evolve very, very quickly.

Best practice tip

Give components a single responsibility and functionality. Avoid mammoth monolithic components as much as possible.

Encapsulation

Encapsulation is the notion that you should wrap data and methods to act as a single unit while exposing a well-defined application programming interface (API). Often, this is done in the form of classes, modules, or libraries. JavaScript is not an exception, and it is highly recommended to follow this principle. In Vue 3, this concept applies to not only components but also CSS styles and HTML. The introduction of single-file components is a clear example of how the framework promotes this principle in action and how important it is for today’s development. With only a few edge-case situations, we should consider the (UI) components as black boxes that receive incoming parameters and provide outgoing data. Other components should not be aware of their inner workings, only the API. As we build example applications throughout this book, you will see this principle in action.

KIC – keep it clean

This principle refers mainly to the way you write code. I should emphasize here that KIC applies directly to two categories that strongly affect web and Vue 3 applications:

  • How you format your code
  • How you tidy up events and variables

The first item includes the use of code conventions, comments, and indentation to the organization of the code and logical grouping of functions. For example, if you have methods that deal with create, read, update, and delete (CRUD) operations, it would be best to place them near each other in the code, rather than spread around the source file. Many integrated development environments (IDEs) contain features to collapse or expand the inner code of functions. This helps to quickly review and locate sections in the code with similar logic.

The second part of this principle has to do with memory and reference handling. JavaScript has a very good garbage collector, the function of which is to discard unused data to reclaim memory. However, there are occasions when the algorithm is prevented from freeing up resources because a reference is still pending. If you have worked with other languages, such as C/C++, this issue may sound familiar as you need to manually reserve and release memory when not in use. In JavaScript, if you register a function to listen to an event, it is best to manually deregister it at the appropriate life cycle event of your component when no longer needed. This will prevent memory leaks and waste of memory and also prevent some security risks (which are out of the scope of this book).

We will review the component’s life cycle in Chapter 4, User Interface Composition with Components, but for now, take the following example as a good application of this principle and keep it as best practice. In this example, we will create a composable component to detect when the window size changes, so in the script setup section we will find something like this:

  1. Registers a function on the window object’s resize event during the mounting state.
  2. Deregisters the event before the component is unmounted.

Here is the code fragment:

<script setup>
   import {onMounted, onBeforeUnmount} from "vue"
   onMounted(()=>{
       window.addEventListener("resize", myFunction)
   })
   onBeforeUnmount(()=>{
       window.removeEventListener("resize", myFunction)
   })
   function myFunction(){
       // Do something with the event here
   }
</script>

The onMounted and onBeforeUnmount functions are part of the Vue 3 framework and are triggered by the appropriate component life cycle event. Here, we attach our function to the resize event when the component is mounted to the Document Object Model (DOM), and we release it just before it is removed. The important concept to remember is to clean up after yourself and keep it clean.

DRY – don’t repeat yourself

This principle is quite famous, almost to the point of turning into a cliché. Sadly, it is easily forgotten. It is credited to Andrew Hunt and David Thomas, who used it in the book The Pragmatic Programmer. It is mostly thought of as don’t write the same thing twice and is not far off, but it goes beyond that. It encompasses the notion of avoiding redundancy in the process as well as in the logic of the application. The core idea is that each process that executes business logic should exist in only one place in your entire application.

For example, most web applications have some asynchronous connection with a server through the use of an API. There may also be multiple elements in the application that will use or need to use this remote computer/server communication. If you were going to code the entire code/logic to communicate with the server in each component, we would end up with not only duplication of code but also application logic. Maintaining such a system would open up the door to an amazing number of negative side effects and security concerns, poor user experience, and much more. According to this principle, a better approach is to abstract all communication code related to the server API into a single module, or class. In practice, in JavaScript this can even be delegated to a web worker in a separate thread. We will explore this implementation later in Chapter 8, Multithreading with Web Workers.

As a rule of thumb, if you see yourself writing kind-of-the-same-code” in different components or classes, it is a clear opportunity to abstract the functionality into its own module or component.

KISS – keep it simple and short

This principle is not exclusive to the software design realm. It was coined by the US Navy back in the ’60s (according to Wikipedia, https://en.wikipedia.org/wiki/KISS_principle). The idea is pure common sense: it is better to build simple, small functional parts that work together than attempt to create a big and complex program in one go. Also, algorithms should be implemented in the most simple and efficient way. In web development, this principle is essential. Modern web applications are composed of hundreds of working parts spread over multiple computers, servers, and environments. The more complex a system or code implementation is, the harder it is also to maintain and adapt.

There is a warning, though. Keeping things simple does not mean over-simplification or unnecessary segregation. Too many small parts can introduce unnecessary complexity in the system. Applying the KISS principle means staying in that sweet middle point where things are manageable and easy to understand.

Code for the next

This principle is the idea that you should make your code readable and easy to understand for someone else besides you. Naming conventions, logic flow, and inter-line comments are all part of this. Not only for the case when you may need to delegate your code to another but also when you come back in a year or two to the same code. The last thing you want to do is to waste time thinking about what the past inexperienced you did with that clever line of spaghetti code Smart developers code as if they were going to teach somebody else, simply and elegantly. Especially if you are using or contributing to open-source code, this principle is vital for group collaboration. In this case, it is worth mentioning the Boy Scout Principle, which is similar but applies in groups. It states that when you find a hard-to-read or “spaghetti” code, you refactor it to make it clean.

Best practice tip

Keep your code clean with on-source comments and documentation explaining your logic, as if teaching somebody else. More often than not, you will be teaching yourself.

Design principles apply to many different scenarios, some beyond the practice of software development. It is important to consider them until they become second nature. In general, the application of these and other principles, together with the application of design patterns, make an important mark on your professional development.

What is a software design pattern?

In software development, it is common for certain processes and tasks to appear in multiple projects, in one way or another, or with some degree of variation. A design pattern is a proven solution for such similar problems. It does not prescribe code but acts like a reasoning template, an approach that has been abstracted independent of the implementation to be reused and adapted to particular circumstances. In practice, there is plenty of room for creativity to apply a pattern. Entire books have been dedicated to this subject and provide more detail than the scope of this book allows. In the following pages, we will take a look at what I consider to be the most recurrent patterns to keep in mind for Vue 3 applications. Even though we see them in isolation for the purposes of studying them, the reality is that often the implementation overlaps, mixes, and encapsulates multiple patterns in a single piece of code. For example, you can use a singleton to act as a decorator and a proxy to simplify or alter the communication between services in your application (we will do this quite often, actually, and the full code can be seen in Chapter 8, Multithreading with Web Workers).

Design patterns can also be understood as software engineering and development best practices. And the opposite of that, bad practice, is often referred to as an anti-pattern. Anti-patterns are “solutions” that, even though they fix an issue in the short term, create problems and bad consequences along the line. They generate the need to work around the problem and destabilize the whole structure and implementation.

Let’s now view a list of patterns that should be part of your toolbox for Vue 3 projects.

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.

Summary

In this chapter, we have seen principles for software development and important design patterns, with examples in plain JavaScript and, when appropriate, hinted at implementations with Vue 3. These patterns can be hard to grasp the first time you see them, but we will use them and return to them in the rest of the book so that this chapter will work as a reference. This will give you a better idea of when and how to apply different patterns according to the needs of your application.

In the next chapter, we will start to implement a project from scratch and will set the foundations for the applications we will build in the rest of the book. As we move forward, we will reference these patterns to help you consolidate their application.

Review questions

  • What is the difference between a principle and a pattern?
  • Why is the singleton pattern so important?
  • How can you manage dependencies?
  • What patterns make reactivity possible?
  • Do patterns intertwine? Why? Can you give an example?
  • What is asynchronous programming, and why is it so important?
  • Can you think of use cases for promised functions?
Left arrow icon Right arrow icon
Download code icon Download Code

Key benefits

  • Learn software engineering best practices and design patterns and apply them effectively to your Vue applications
  • Build both SPAs and PWAs using Web Workers and IndexedDB
  • Develop, test, build, and deploy your Vue 3 applications to a real production server

Description

If you’re familiar with the progressive Vue framework for creating responsive user interfaces, you’ll be impressed with its latest iteration, Vue 3, which introduces new concepts and approaches design patterns that are uncommon in other libraries or frameworks. By building on your foundational knowledge of Vue 3 and software engineering principles, this book will enable you to evaluate the trade-offs of different approaches to building robust applications. This book covers Vue 3 from the basics, including components and directives, and progressively moves on to more advanced topics such as routing, state management, web workers, and offline storage. Starting with a simple page, you’ll gradually build a fully functional multithreaded, offline, and installable progressive web application. By the time you finish reading this Vue book, not only will you have learned how to build applications, but you’ll also understand how to solve common problems efficiently by applying existing design patterns. With this knowledge, you’ll avoid reinventing the wheel for every project, saving time and creating software that’s adaptable to future changes.

Who is this book for?

This book is for Vue.js developers who are passionate about framework design principles and seek to apply commonly found design patterns to their web development projects. This book assumes prior knowledge of JavaScript and a basic understanding of Vue.js, making it an ideal resource for developers looking to expand their existing skillset.

What you will learn

  • What is the Vue 3 progressive framework
  • What are software principles and design patterns, how and when to implement them, and the trade-offs to consider
  • Setup your development environment using the new Vite bundler
  • Integrate in your applications state management, routing, multithreading, offline storage, and other resources provided to you by the browser, seldom taken advantage
  • Apply and identify design patterns to solve common problems in the architecture of your web application
  • Best practices for your code, organization, architecture, and user experience implementation
  • Incrementally expand an application with new functionalities without re-writing the whole application each time

Product Details

Country selected
Publication date, Length, Edition, Language, ISBN-13
Publication date : May 30, 2023
Length: 296 pages
Edition : 1st
Language : English
ISBN-13 : 9781803238074
Category :
Languages :
Tools :

What do you get with a Packt Subscription?

Free for first 7 days. ₹800 p/m after that. Cancel any time!
Product feature icon Unlimited ad-free access to the largest independent learning library in tech. Access this title and thousands more!
Product feature icon 50+ new titles added per month, including many first-to-market concepts and exclusive early access to books as they are being written.
Product feature icon Innovative learning tools, including AI book assistants, code context explainers, and text-to-speech.
Product feature icon Thousands of reference materials covering every tech concept you need to stay up to date.
Subscribe now
View plans & pricing

Product Details

Publication date : May 30, 2023
Length: 296 pages
Edition : 1st
Language : English
ISBN-13 : 9781803238074
Category :
Languages :
Tools :

Packt Subscriptions

See our plans and pricing
Modal Close icon
₹800 billed monthly
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Simple pricing, no contract
₹4500 billed annually
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just ₹400 each
Feature tick icon Exclusive print discounts
₹5000 billed in 18 months
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just ₹400 each
Feature tick icon Exclusive print discounts

Frequently bought together


Stars icon
Total 9,681.97
Vue.js 3 Design Patterns and Best Practices
₹2978.99
Frontend Development Projects with Vue.js 3
₹3723.99
Architecting Vue.js 3 Enterprise-Ready Web Applications
₹2978.99
Total 9,681.97 Stars icon

Table of Contents

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

Customer reviews

Top Reviews
Rating distribution
Full star icon Full star icon Full star icon Full star icon Half star icon 4.8
(10 Ratings)
5 star 90%
4 star 0%
3 star 10%
2 star 0%
1 star 0%
Filter icon Filter
Top Reviews

Filter reviews by




Wellington Mekhoe Jan 18, 2024
Full star icon Full star icon Full star icon Full star icon Full star icon 5
Feefo Verified review Feefo
Kirill Sakun Dec 12, 2023
Full star icon Full star icon Full star icon Full star icon Full star icon 5
Feefo Verified review Feefo
Milos Pavlovic Aug 01, 2024
Full star icon Full star icon Full star icon Full star icon Full star icon 5
Feefo Verified review Feefo
Vladyslav Nosal Jun 15, 2023
Full star icon Full star icon Full star icon Full star icon Full star icon 5
I really liked the content as it enables to both explore new things and look at already established practice from a different standpoint. I would say that I was please to see different patterns and their employment in scope of Vue and sometimes I encountered really cool information which is either not very easy to find or is not so popular across the web. In addition to that, I was also happy to learn more about PWA as I really thing this is a very promising direction of web development, and book covers everything from the setting it up to powering it up with multhreading.
Amazon Verified review Amazon
Anthony Jun 16, 2023
Full star icon Full star icon Full star icon Full star icon Full star icon 5
"Vue.js 3 Design Patterns and Best Practices" by Pablo David Garaguso is an indispensable resource for Vue developers looking to level up their skills and build robust applications. Garaguso's book provides a comprehensive guide that covers essential design patterns and best practices, empowering developers to write clean, maintainable code.One of the book's greatest strengths is its organization. Garaguso starts with an introduction to the core concepts of Vue.js 3, ensuring readers have a solid foundation before delving into design patterns. Each pattern is introduced with a clear explanation and then showcased through a step-by-step implementation process. This systematic approach enables developers to grasp the concepts gradually and apply them effectively.Furthermore, the book emphasizes best practices, teaching readers how to write clean and efficient code. Garaguso highlights common pitfalls and provides tips for optimizing performance, improving reusability, and organizing project structure. These best practices not only enhance the quality of your code but also contribute to a smoother development process and better collaboration within teams.In conclusion, "Vue.js 3 Design Patterns and Best Practices" is an exceptional book that should be on every Vue.js developer's shelf. Whether you're a novice or an experienced developer, the book will empower you to write cleaner, more maintainable code and unlock the full potential of Vue 3. Highly recommended!
Amazon Verified review Amazon
Get free access to Packt library with over 7500+ books and video courses for 7 days!
Start Free Trial

FAQs

What is included in a Packt subscription? Chevron down icon Chevron up icon

A subscription provides you with full access to view all Packt and licnesed content online, this includes exclusive access to Early Access titles. Depending on the tier chosen you can also earn credits and discounts to use for owning content

How can I cancel my subscription? Chevron down icon Chevron up icon

To cancel your subscription with us simply go to the account page - found in the top right of the page or at https://subscription.packtpub.com/my-account/subscription - From here you will see the ‘cancel subscription’ button in the grey box with your subscription information in.

What are credits? Chevron down icon Chevron up icon

Credits can be earned from reading 40 section of any title within the payment cycle - a month starting from the day of subscription payment. You also earn a Credit every month if you subscribe to our annual or 18 month plans. Credits can be used to buy books DRM free, the same way that you would pay for a book. Your credits can be found in the subscription homepage - subscription.packtpub.com - clicking on ‘the my’ library dropdown and selecting ‘credits’.

What happens if an Early Access Course is cancelled? Chevron down icon Chevron up icon

Projects are rarely cancelled, but sometimes it's unavoidable. If an Early Access course is cancelled or excessively delayed, you can exchange your purchase for another course. For further details, please contact us here.

Where can I send feedback about an Early Access title? Chevron down icon Chevron up icon

If you have any feedback about the product you're reading, or Early Access in general, then please fill out a contact form here and we'll make sure the feedback gets to the right team. 

Can I download the code files for Early Access titles? Chevron down icon Chevron up icon

We try to ensure that all books in Early Access have code available to use, download, and fork on GitHub. This helps us be more agile in the development of the book, and helps keep the often changing code base of new versions and new technologies as up to date as possible. Unfortunately, however, there will be rare cases when it is not possible for us to have downloadable code samples available until publication.

When we publish the book, the code files will also be available to download from the Packt website.

How accurate is the publication date? Chevron down icon Chevron up icon

The publication date is as accurate as we can be at any point in the project. Unfortunately, delays can happen. Often those delays are out of our control, such as changes to the technology code base or delays in the tech release. We do our best to give you an accurate estimate of the publication date at any given time, and as more chapters are delivered, the more accurate the delivery date will become.

How will I know when new chapters are ready? Chevron down icon Chevron up icon

We'll let you know every time there has been an update to a course that you've bought in Early Access. You'll get an email to let you know there has been a new chapter, or a change to a previous chapter. The new chapters are automatically added to your account, so you can also check back there any time you're ready and download or read them online.

I am a Packt subscriber, do I get Early Access? Chevron down icon Chevron up icon

Yes, all Early Access content is fully available through your subscription. You will need to have a paid for or active trial subscription in order to access all titles.

How is Early Access delivered? Chevron down icon Chevron up icon

Early Access is currently only available as a PDF or through our online reader. As we make changes or add new chapters, the files in your Packt account will be updated so you can download them again or view them online immediately.

How do I buy Early Access content? Chevron down icon Chevron up icon

Early Access is a way of us getting our content to you quicker, but the method of buying the Early Access course is still the same. Just find the course you want to buy, go through the check-out steps, and you’ll get a confirmation email from us with information and a link to the relevant Early Access courses.

What is Early Access? Chevron down icon Chevron up icon

Keeping up to date with the latest technology is difficult; new versions, new frameworks, new techniques. This feature gives you a head-start to our content, as it's being created. With Early Access you'll receive each chapter as it's written, and get regular updates throughout the product's development, as well as the final course as soon as it's ready.We created Early Access as a means of giving you the information you need, as soon as it's available. As we go through the process of developing a course, 99% of it can be ready but we can't publish until that last 1% falls in to place. Early Access helps to unlock the potential of our content early, to help you start your learning when you need it most. You not only get access to every chapter as it's delivered, edited, and updated, but you'll also get the finalized, DRM-free product to download in any format you want when it's published. As a member of Packt, you'll also be eligible for our exclusive offers, including a free course every day, and discounts on new and popular titles.