• Ramotion /
  • Blog /
  • The Most Common Frontend Design Patterns Explained

The Most Common Frontend Design Patterns Explained

Knowing how to use design patterns makes your code cleaner and more understandable. This article teaches various frontend design patterns.

Written by RamotionApr 28, 202322 min read

Last updated: Sep 4, 2024

Frontend design patterns are UX-related code solutions for dealing with specific situations. They're best practices and a good way to boost your skill set and design adeptness. One could argue that they aren't only for pros, as newcomers can get familiarized with them as well. Even if you're new in this field, it's still okay. In fact, now's the perfect time to start.

The pattern is a reusable solution to a commonly occurring problem in software design. It's an approach or strategy that can be used in many different types of projects, on any scale or complexity. Design patterns are often not unique to front-end development but they can be applied here as well.

Design patterns are essentially guides showing how the front end might be structured to solve common user problems. These design patterns exist to optimize work processes and make it easier for users to interface with the website and frontend development services to build frontend applications with ease. There are many different types of design patterns and examples that exist. This article attempts to explain the most commonly used design patterns.

Defining Design Patterns in Frontend Development

The idea behind a pattern is that you can use it as a template and adapt it to your particular problem. A pattern is not just an idea or an example of how something works; rather, it describes the problem in detail, along with its context and consequences. Then, the solution can be described in detail as well - including how it works and what tradeoffs to consider when using it.

Patterns are not rules or laws. They’re not something that you can blindly follow and expect to get the desired result every time. Instead, they represent a set of tradeoffs for different situations. Patterns are about context: in order to use a pattern effectively, you need to understand what problem is being solved and how it fits with the rest of your system. Patterns are also not a substitute for good design. They provide a way of thinking, not writing code. Use them as a starting point to help guide your design decisions - but don’t stop there.

When designing an application, it is important to have a good understanding of the business, user requirements, and available technologies. On the other hand, design patterns are not tied to specific technologies or platforms. Rather, they provide abstractions that can be used when developing the application. This makes them highly useful in all kinds of projects.

Another advantage of using design patterns is that they make code more readable and maintainable. This is because the patterns are designed to solve certain problems in a generic way, so once you understand how they work, you can apply them to any situation.

Frontend Design Patterns Categories

Some popular design pattern categories:

Creational patterns

Creational patterns are concerned with object creation. They help programmers deal with the problem of how to instantiate objects in such a way that their instances can be configured according to an external specification. The two most common creational patterns are the factory pattern and the builder pattern. The factory pattern allows for a class to create an instance of another class without specifying the exact class to be instantiated.

The purpose of creational design patterns is to create objects of a particular type in an easy and flexible manner. These patterns provide ways to instantiate objects without having to specify the exact class of object that should be created.

The 6 types of creational design patterns are:

1. Factory Method pattern

The Factory Method pattern is an object creation mechanism that uses a factory method to encapsulate the logic for obtaining a specific type of object. This allows a single interface to be used to create many different types of objects.

The basic idea behind this design pattern is that you have a base class with an abstract method (that is, a method without a body), which makes it possible for subclasses of this class to override this method and return an instance of their own subclass. The Factory Method will then call the overridden concrete implementation and return the created object.

Example:

class Robot {
    constructor (name, phrase) {
        this.name = name
        this.phrase = phrase
        this.model = "Tuv"
    }
    walk = () => console.log("Walking!!!")
    sayPhrase = () => console.log(this.phrase)
}

const robot = new Robot("Olivia", "Hi, I'm Olivia!")
console.log(robot.name) // output: "Olivia"
Copy

2. Abstract Factory pattern

In software engineering, the abstract factory pattern is a software design pattern that uses factory methods to deal with the problem of creating objects without having to specify their concrete classes. This is done by defining an interface for creating related objects and varying their implementation. Abstract factories are often used for creating families of related or dependent objects without specifying their concrete classes.

The idea behind this pattern is to create an interface for creating families of related or dependent objects without specifying their concrete classes. This way, you can decouple the classes you want to create from the concrete classes themselves.

An abstract factory is used when you have a family of similar or related objects, and you want a way to group and manage the family as one entity. Abstract Factory provides an interface for creating families of related or dependent objects without specifying their concrete classes.

3. Builder pattern

The builder pattern is used when we want to create complex objects but don’t want to expose the internals of those objects. The builder pattern allows for the creation of objects in steps, where each step returns a modified version of the final object.

It centralizes all of the shared initialization logic into one location. This makes your code easier to maintain because you have fewer places where you need to perform this same initialization work.

Example: (Example taken from freeCodeCamp)

// We declare our objects
const bug1 = {
    name: "Buggy McFly",
    phrase: "Your debugger doesn't work with me!"
}

const bug2 = {
    name: "Martiniano Buggland",
    phrase: "Can't touch this! Na na na na..."
}

// These functions take an object as parameter and add a method to them
const addFlyingAbility = obj => {
    obj.fly = () => console.log(`Now ${obj.name} can fly!`)
}

const addSpeechAbility = obj => {
    obj.saySmthg = () => console.log(`${obj.name} walks the walk and talks the talk!`)
}

// Finally we call the builder functions passing the objects as parameters
addFlyingAbility(bug1)
bug1.fly() // output: "Now Buggy McFly can fly!"

addSpeechAbility(bug2)
bug2.saySmthg() // output: "Martiniano Buggland walks the walk and talks the talk!"
Copy

4. Singleton Pattern

The singleton pattern is used when we want to restrict the instantiation of a class to one object. The singleton pattern ensures that only one instance of a class exists, and provides a global point of access to it.

The common use of the Singleton pattern is for global access. You may have several classes that need access to the same resource, and it would be cumbersome if each instance had to check whether another instance already had it before acquiring it itself. Using a Singleton class, you can ensure that only one instance exists at any time, making it easier for clients to acquire the resource they require.

Example:

class Config {
    constructor() {}
    start(){ console.log('App has started') }  
    update(){ console.log('App has updated') }
}
  
const instance = new Config()
Object.freeze(instance)
Copy

5. Prototype pattern

The Prototype design pattern is used to create a new instance of an object using a object prototype to do the actual creation. This pattern is useful when you have complex objects that have to be created and don’t want to create all the code required for your custom object at the same time.

This is a very important concept in programming because it allows us to make changes without affecting other parts of our system. This makes it easier to update entire systems without affecting other parts of our system that we may not even be aware of yet!

6. Object Pool pattern

The object pool pattern is used when we want to minimize the usage of an expensive resource by recycling objects that are no longer in use. The object pool pattern maintains a pool of reusable objects and allows us to create new objects without incurring the cost of creating them from scratch each time.

The main advantage of this pattern is that it reduces memory consumption by reusing objects instead of creating new ones every time they’re required. If you have ever used an IDE, you will notice that every time you compile your code, it takes quite some time to load all the classes into memory. The problem here is that each class has its own copy in memory, even if it doesn’t need one since it

Structural patterns

Structural patterns are a group of patterns that describe the relationships between entities and how those relationships should be formed. They help us to determine how our objects should interact with each other, which makes it easier for us to design clean and flexible systems.

The structural design patterns are classic solutions to recurring problems in object-oriented systems. They help programmers to organize their code in a way that’s flexible and easy to maintain, even as the system evolves over time. Structural design patterns can be applied at different levels of abstraction, from low-level details of how classes are used to high-level organization of entire systems.

The 7 types of Structural design patterns:

1. Adapter pattern

Adapter pattern is a software design pattern that allows the interface of an existing class to be used as another interface. It is often used to make existing classes work with other classes without modifying the original class or to implement multiple inheritances in a way that does not require subclassing.

The adapter pattern has two basic forms: class adapter and object adapter. The class adapter is applicable when you have a single interface that needs to be implemented by different classes. Object adapter is useful when you have one or more interfaces that need to be implemented by a single class.

Example:

function RedisDataManager() {
    this.connect = function() {
        console.log('Connect to database');
    };
    this.scan = function() {
        return 'Data from database';
    };
}
function DataManager() {
    this.getData = function() {
        return 'Legacy data';
    }
}
function Adapter() {
    var redis = new RedisDataManager();
    redis.connect();
    this.getData = function() {
        return redis.scan();
    }
}
function Client(dataManager) {
    console.log(dataManager.getData());
}
var client = new Client(new Adapter());
Copy

2. Bridge pattern

Bridge design pattern allows you to decouple an abstraction from its implementation so that the two can vary independently. It's particularly useful when the code that uses an abstraction layer may need to be changed, but the actual code implementing that abstraction should not.

The Bridge pattern can be implemented using inheritance. The primary goal of this pattern is to reduce coupling between classes that have strong dependencies on each other by introducing a new class in between them as a mediator or “bridge” between these classes. The bridge class implements both interfaces and forwards requests from one interface to another through method calls. This way, both interfaces remain independent because they don't have any knowledge of each other and no direct coupling exists between them.

Example:

// input devices

function Gestures(output) {
    this.output = output;

    this.tap = function () { this.output.click(); }
    this.swipe = function () { this.output.move(); }
    this.pan = function () { this.output.drag(); }
    this.pinch = function () { this.output.zoom(); }
};

function Mouse(output) {
    this.output = output;

    this.click = function () { this.output.click(); }
    this.move = function () { this.output.move(); }
    this.down = function () { this.output.drag(); }
    this.wheel = function () { this.output.zoom(); }
};

// output devices

function Screen() {
    this.click = function () { console.log("Screen select"); }
    this.move = function () { console.log("Screen move"); }
    this.drag = function () { console.log("Screen drag"); }
    this.zoom = function () { console.log("Screen zoom in"); }
};

function Audio() {
    this.click = function () { console.log("Sound oink"); }
    this.move = function () { console.log("Sound waves"); }
    this.drag = function () { console.log("Sound screetch"); }
    this.zoom = function () { console.log("Sound volume up"); }
};

function run() {

    var screen = new Screen();
    var audio = new Audio();
    
    var hand = new Gestures(screen);
    var mouse = new Mouse(audio);

    hand.tap(); // Screen select
    hand.swipe(); // Screen move
    hand.pinch(); // Screen zoom in

    mouse.click(); // Sound oink
    mouse.move(); // Sound waves
    mouse.wheel(); // Sound volume up
}
Copy

3. Composite pattern

The Composite Pattern is used to represent tree structure. The composite pattern defines a hierarchy of classes, called composite classes, whose instances are responsible for handling the details of data storage and retrieval for their parent and child function nodes in the tree structure.

The main idea behind the Composite Pattern is to have a base object that holds some kind of reference to other objects. These other objects are called leaf nodes and they cannot contain any references themselves.

4. Decorator pattern

A decorator is an object that wraps a target object and adds additional functionality or information to it. The decorator pattern lets you dynamically change an object's behavior at run time by wrapping it in an object of a decorator class. The decorator has no knowledge of the internals of the original object; instead, it simply wraps the original and provides additional functionality.

The decorator pattern is very similar to JavaScript inheritance, but instead of subclassing and extending functionality, it wraps objects in order to provide additional functionality. This can be useful when we want to add additional responsibilities to an existing object without having to inherit from it.

Decorator patterns are very useful when you want to add some specific functionality to your functions or methods. The decorator pattern uses an object-oriented programming approach where you pass function (decorated function) as an argument to another function which then wraps the original function with new additional logic before returning it.

Example:

function User(name) {
    this.name = name;

    this.say = function () {
        console.log("User: " + this.name);
    };
}

function DecoratedUser(user, street, city) {
    this.user = user;
    this.name = user.name;  // ensures interface stays the same
    this.street = street;
    this.city = city;

    this.say = function () {
        console.log("Decorated User: " + this.name + ", " +
            this.street + ", " + this.city);
    };
}

function run() {

    var user = new User("Olivia");
    user.say(); // User: Olivia

    var decorated = new DecoratedUser(user, "Broadway", "New York");
    decorated.say(); // Decorated User: Olivia, Broadway, New York
}
Copy

5. Flyweight pattern

The Flyweight design pattern is a software design pattern that allows the same instance of an object to be used many times in a computer application. This is done by sharing the actual object in a flyweight factory or cache, rather than creating new copies as needed. The flyweight object can then act as a master for creating lightweight representations. This saves memory and time during the creation of objects. It is useful when there are many similar objects but only one of these objects is needed at any given time.

A flyweight acts as a shared instance for a set of objects, so that these objects can be accessed without creating redundant copies. The flyweight pattern provides a way to create objects that are not necessarily unique, such as those representing commonly used items like strings and images.

The flyweight pattern is ideal when you need to create many instances of an object but don't want to pay the price of creating many separate copies of its underlying data structure. It's also useful when you need to keep track of many active instances (for example, when performing service transactions).

6. Facade pattern

A facade is an object that provides a simplified interface to a larger body of code, such as a class library. The facade can be used to provide easier access to the subsystem, or it may be intended as a simpler front end to a more complex subsystem.

The Facade pattern is useful when you want to provide a simple interface to a complex subsystem. It can be used when you have several related classes that are difficult to use correctly and you want to provide simpler access to these classes.

7. Proxy pattern

A proxy pattern is an object that acts as a proxy for the real subject. It provides a different interface than the real subject, and it can also keep a transient state that is not shared with the real subject.

The Proxy class implements the same interface as its namesake, but it's also used to encapsulate a reference to another object and provide indirect access to it. This allows for greater flexibility in how you interact with the object and improves code reuse.

Behavioral patterns

Behavioral design patterns are a way of decoupling the presentation from the business logic by isolating the behavior into its own class. This way, we can change the implementation without affecting any other part of our application.

There are several behavioral design patterns deal with separating our code into smaller, more manageable chunks:

1. Chain of Responsibility Pattern

The Chain of Responsibility pattern describes how to pass a request between a chain of processing objects. It's useful when you have a large object hierarchy, and you don't want to hard-code the names of intermediate classes in your client code.

The Chain of Responsibility Pattern is useful when your application has multiple objects that must cooperate to complete a task and you want to ensure that each object in the chain is only asked for help when it can provide assistance. It's also an alternative to using multiple inheritance.

function Request(amount) {
    this.amount = amount;
    console.log("Requested: $" + amount + "\n");
}

Request.prototype = {
    get: function (bill) {
        var count = Math.floor(this.amount / bill);
        this.amount -= count * bill;
        console.log("Dispense " + count + " $" + bill + " bills");
        return this;
    }
}
function run() {
    var request = new Request(378);

    request.get(100).get(50).get(20).get(10).get(5).get(1);
}
Copy

Above code outputs:

Requested: $378
Dispense 3 $100 bills
Dispense 1 $50 bills
Dispense 1 $20 bills
Dispense 0 $10 bills
Dispense 1 $5 bills
Dispense 3 $1 bills
Copy

2. Command Pattern

The command pattern allows a sender to encapsulate a command request as an object, thereby letting the receiver decide how to execute the command at a later time. This lets you decouple the sender from the receiver, which increases flexibility in your system.

The Command pattern has three main classes: the invoker, the receiver, and the command. The invoker is responsible for creating and executing commands. It can also be used to queue commands for later execution or undo commands that have already been executed. The receiver is responsible for receiving and processing a particular command. This can be done by delegating to another class or method in your application. Finally, the command itself is an interface or abstract class that contains a method with no parameters that returns void. The command also contains any parameters needed to execute it such as data types of input and output parameters.

3. Interpreter Pattern

Interpreter Pattern is a design pattern that allows the creation of language-independent software. It provides an interface to evaluate and execute multiple languages. The Interpreter Pattern makes use of rules and grammar to translate the user input to the output.

Interpreters are used in many applications that need to evaluate complex commands or expressions. They're also used to parse input strings into data structures such as trees or graphs. The expression evaluator is one of the simplest examples of an interpreter, but it's also available in many languages and frameworks as part of their standard library.

4. Iterator Pattern

The Iterator Pattern is a design pattern that allows you to sequentially access the elements of a container. It also allows you to traverse the elements in either direction, and it lets you remove elements from the container while you’re iterating over it. The iterator itself is responsible for generating the next element in the sequence.

It is used when you have an object that acts as a container for other objects. You need to provide an interface that allows users to traverse or iterate over the elements of this collection. The Iterator interface provides a simple protocol for traversing and exposing the elements of a collection, without requiring any of its clients to be aware of its concrete implementation.

Example:

function Iterator(items) {
    this.index = 0;
    this.items = items;
}

Iterator.prototype = {
    first: function () {
        this.reset();
        return this.next();
    },
    next: function () {
        return this.items[this.index++];
    },
    hasNext: function () {
        return this.index <= this.items.length;
    },
    reset: function () {
        this.index = 0;
    },
    each: function (callback) {
        for (var item = this.first(); this.hasNext(); item = this.next()) {
            callback(item);
        }
    }
}

function run() {

    var items = ["one", 2, "circle", true, "Applepie"];
    var iter = new Iterator(items);

    // using for loop

    for (var item = iter.first(); iter.hasNext(); item = iter.next()) {
        console.log(item);
    }
    console.log("");

    // using Iterator's each method

    iter.each(function (item) {
        console.log(item);
    });
}
Copy

5. Mediator Pattern

The Mediator pattern is a software design pattern, used to reduce coupling and increase flexibility by introducing a third-party object that mediates access between two or more objects. A mediator behaves as a proxy for the objects it coordinates, shielding them from each other. The mediator may also shield the software system from changes in the subsystems it coordinates. The mediator plays an important role in keeping the system loosely coupled, decoupling the various elements of the system so that they are easier to maintain and replace.

The core idea behind the Mediator Pattern is that there should be only one instance of the mediator class for each number of objects being mediated. The mediator class acts as an interface between objects and delegates method calls to them.

6. Memento Pattern

Memento pattern can be used to create a simple version of the State (of an object) and restore it at a later time. It is one of the twenty-three well-known GoF design patterns that describe how to solve recurring design problems to design flexible and reusable object-oriented software, that is, objects that are easier to implement, change, test, and reuse.

A Memento is an object that stores state information about another object in such a way that the original object can be restored. A memento can be thought of as a backup copy of the original data. It may contain additional information about the state of an object, such as the time at which it was stored, but does not contain any references to other objects or resources outside itself.

In addition to saving and restoring state, a memento can also help you avoid unnecessary repetition of code. For example, if you have an object that represents a person or any entity that has properties like name, age, email address, etc., then if you want to display all these properties in HTML table format (i.e. row per property), then instead of repeating code everywhere where this table needs to be displayed, you just need to create a single method which will return desired HTML table based on input parameters (i.e., values for each property).

7. Observer Pattern

The Observer Pattern is a software design pattern in which an object, called the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods. This allows the subject to broadcast changes to a number of dependents without knowing anything about them or how they will use the information.

The idea behind this pattern is to provide a single centralized interface for objects to register with so that these objects can be easily plugged into the system at run time. The observer may be registered with multiple subjects and will receive notifications from each subject even if they are not related to each other.

8. State Pattern

The State Pattern defines a way to encapsulate the concept of "state" in an object, and transition between states. An object can be in one of several different states, and it changes its behavior depending on what state it is in. This pattern allows you to vary behavior in response to external events without changing the code that implements your objects' behaviors.

The concept of State Pattern can be explained with an example. Consider a class called “Car” which has some states like “started”, “stopped”, “idling” etc. In this case, an instance of a Car can have any one of these states at any given time. Now consider another class called “Driver” who drives the car by controlling its speed and direction and hence needs to know about all possible states of the car so that he can control it appropriately in each state. This is where State Pattern comes into the picture where we create a separate interface for each possible state and extend it from our main interface which has all common methods and properties like start(), stop(), etc.

9. Strategy Pattern

Strategy Pattern is a design pattern that provides the ability to change the behavior of an algorithm at runtime. It allows you to write a family of algorithms, each of which is encapsulated in an object, and have all algorithms in the family share a common interface. This means that you can change the implementation of a group of algorithms without changing any code outside the group. The Strategy Pattern is one of the most popular design patterns among software developers and has been used by many major software companies including Microsoft, Google, Facebook, and many others.

This is achieved by encapsulating multiple algorithms (Strategies) in a single interface, then dynamically selecting the algorithm at runtime based on the context of the request.

The advantage of this pattern is that it allows you to change the behavior of your application without having to modify existing code. This makes this pattern particularly useful when developing a component that needs to be highly configurable.

10. Template Pattern

Template Pattern is a design pattern that describes how to instantiate one or more objects based on the same template. This pattern is commonly used to simplify the creation of many similar instances, where the exact properties of these objects may not be known until runtime. It's also used when you need to create many similar objects with only a few properties that differ between them.

The template doesn't contain any data, but it does contain fields that other code can use to store data. When you use the template to create an instance of an object, you provide the data for those fields in the constructor.

The advantage of this approach is that it allows you to create different kinds of objects without changing your source code. For example, if we have a template for a Car object, it will support all kinds of cars: Volkswagens, Porsches, and even Ferraris.

Example:

var datastore = {
    process: function () {
        this.connect();
        this.select();
        this.disconnect();
        return true;
    }
};

function inherit(proto) {
    var F = function () { };
    F.prototype = proto;
    return new F();
}

function run() {
    var mySql = inherit(datastore);

    // implement template steps

    mySql.connect = function () {
        console.log("MySQL: connect step");
    };

    mySql.select = function () {
        console.log("MySQL: select step");
    };

    mySql.disconnect = function () {
        console.log("MySQL: disconnect step");
    };

    mySql.process();
}
Copy

Above code outputs:

MySQL: connect step
MySQL: select step
MySQL: disconnect step
Copy

Advantages of Frontend Design Patterns

  • Frontend design patterns allow you to organize your code into cohesive, reusable object oriented software. This is especially important when working on large-scale websites where there are many developers working together on different components of the site. By using design patterns, you can ensure that all developers are following a common set of guidelines and conventions for writing code.
  • When you use design patterns, you can write code that’s easier to understand and maintain. They also improve code quality. This makes it much easier to add new features or fixes bugs in your site because the code is organized in a way that makes it easy to find what you’re looking for (and where).
  • The main goal of front-end development is to create an attractive and intuitive interface for users. But there are many other challenges that need to be addressed in this process. For example, there is a need to ensure consistency across all components and pages in your application — otherwise, it would be difficult for users to find their way around the website or app.
  • Frontend design patterns can help you with this task. They help you make sure that all components fit together seamlessly and follow similar rules when it comes to colors, fonts, or layouts. When used properly, frontend design patterns also make the code easier to read and understand for other developers who might join the project later on.
  • Frontend design patterns can also help you create a consistent user experience throughout your application. This means that users will be able to navigate through the website or app without thinking too hard about how they do it.

Summary

Design patterns are tried-and-true fixes for typical issues in software development. They offer a model for how to address the issue and are applicable in a variety of circumstances. A design pattern addresses the problem in-depth, complete with its context and effects, rather than just offering an idea or an illustration of how something functions.

A collection of patterns known as creational design patterns explains how relationships between entities should be constructed. A class can make an instance of another class using the factory pattern without stating the specific class that should be created. At various levels of abstraction, from the low-level specifics of how classes are used to the high-level structuring of entire systems, structural design patterns can be implemented.

This leads me to my ultimate point: design patterns should be used to save time and energy, not reinvent the wheel with every new project. Instead, use design patterns as a guiding light to help you get started and avoid common pitfalls. By implementing the most popular and appropriate patterns in your projects, you’ll be able to focus on the unique aspects of your application instead of wasting time reinventing the wheel.

Building systems, machines, and programs are usually done by following some sort of pattern. These patterns are fundamental ways of solving a problem, which you can use again and again by applying just some general rules. One of the biggest benefits of working like this is that you can build similar things from new with a faster learning curve and less risk of failure because you already know the key points and most problems have already been solved.

There are a lot of things that go into a successful front-end design, and there are even more patterns emerging each year. One should take the time to research these different patterns, and then mix and match them to suit their needs. Whatever you do, just don't be lazy! Reading through several documentation sites, until something sticks, will end in poor results. It may seem like an inordinate amount of work now, but it would save you from paying for it down the road if you simply put forth the effort!

Share: