16 Design Patterns for Javascript Developers
Introduction
Understanding Design Patterns
A design pattern is a general solution to a commonly occurring problem in software design. It is a reusable approach that can be implemented in different situations. Design patterns help developers to organize their code, make it more modular, and improve its overall quality.
Why are Design Patterns so important?
- They help us build software based on the experience of many developers
- They provide us a common vocabulary to describe solutions for different problems.
It’s important to remember that the patterns are not exact solutions for the problem we are facing, rather they provide us with solution schemes which we could follow to solve those problems. Neither do they replace good software designers, rather, they support them.
Anti-Patterns
Anti-Patterns are certain patterns in software development that are considered bad programming practices. If we consider that a pattern represents a best practice, an anti-pattern represents a lesson that has been learned. While it’s quite important to be aware of design patterns, it can be equally important to understand anti-patterns. When creating an application, a project’s life cycle begins with construction; however, once you have the initial release, it needs to be maintained. The quality of a final solution will either be good or bad, depending on the level of skill and time the team has invested in it. Design may be qualified as anti-pattern
if the pattern applied to the problem was in the wrong context.
Here are some examples of Javascript anti-patterns:
-
Polluting the global namespace by defining a large number of variables in the global context.
-
Passing strings rather than functions to either setTimeout or setInterval, as this triggers the use of eval() internally.
-
Modifying the Object class prototype (this is a particularly bad anti-pattern).
-
Using JavaScript in an inline form as this is inflexible.
-
The use of
document.write
where native DOM alternatives such asdocument.createElement
are more appropriate.
Knowledge of anti-patterns is critical for success. Once we are able to recognize such anti-patterns, we’re able to refactor our code to negate them so that the overall quality of our solutions improves instantly.
Types of Design Patterns
There are three main types of design patterns:
-
Creational Patterns: These patterns deal with object creation mechanisms, trying to create objects in a manner suitable for the situation. Some examples of creational patterns include the Singleton, Factory and Prototype patterns.
-
Structural Patterns: These patterns deal with object composition, providing ways to create relationships between objects to form larger structures. Some examples of structural patterns include Flyweight, and Proxy patterns.
-
Behavioral Patterns: These patterns deal with object interaction and responsibility, providing ways to communicate between objects and assign responsibilities to them. Some examples of behavioral patterns include the Observer and Command patterns.
Each of these patterns has its own strengths and weaknesses, and can be used to solve different types of problems in different contexts. By understanding these patterns and knowing when to use them, you can write more efficient and maintainable JavaScript code.
Javascript Design Patterns
Let's discuss the 16 most widely used Design patterns in the Javascript world. Note that some of these patterns may become anti-patterns in the future, others can be modified and/or replaced with new ones.
1. Singleton Pattern
Singletons are classes that are instantiated only once, and only the instance of the class exists throughout the lifetime of the application.
This pattern is useful when dealing with resources that should not be duplicated, such as the global state inside an app, database configuration settings ... .
Let's see the example:
class Singleton {
constructor() {
if (!Singleton.instance) {
// If instance doesn't exist, create it
Singleton.instance = this;
}
return Singleton.instance;
}
sayHello() {
console.log("Hello!");
}
}
// Usage:
const singletonA = new Singleton();
const singletonB = new Singleton();
console.log(singletonA === singletonB); // Output: true
singletonA.sayHello(); // Output: "Hello!"
In this example, we define a class called Singleton with a constructor. The constructor checks whether Singleton.instance
exists. If it does not exist, it creates a new instance of the class and sets it to Singleton.instance. If Singleton.instance
already exists, it simply returns it.
Alternatively we could bring following example:
const Singleton = {
sayHello() {
console.log("Hello!");
},
};
Pros:
- Ensures that only one instance of a class exists throughout the lifetime of the application.
- Provides a global point of access to the instance, making it easy to share data and functionality across different parts of the codebase.
- Can help to improve performance by avoiding the creation of multiple instances of the same object
Cons:
- Can make testing more difficult, as it can be hard to isolate the Singleton instance and test it independently.
- Can make code harder to maintain and debug, as the global state created by the Singleton can make it hard to reason about the behavior of the system.
- Can lead to code bloat, as developers may be tempted to add more and more functionality to the Singleton instance, making it harder to understand and work with over time.
2. Proxy Pattern
Instead of directly interacting with the object we can interact with it's proxy which is kind of a representative of that Object. for example:
const person = {
name: "John Doe",
age: 12,
nationality: "American",
};
const personProxy = new Proxy(person, {
get: (obj, prop) => {
console.log(`The value of prop is ${obj[prop]}`);
},
set: (obj, prop, newVal) => {
console.log(`The old value is ${prop[obj]}, new value is ${newVal}`);
obj[prop] = newVal;
},
});
The second argument of Proxy initialization is an object which represents the handler. In the handler we can define a specific behavior based on the type interaction. There are many methods we could add to proxy but the basic ones are get
and set
.
get
gets invoked when trying to get propertiesset
gets invoked when trying to modify properties
When modifying the person object we get extra notes. This could be useful to implement validation. For example:
const personProxy = new Proxy(person, {
get: (obj, prop) => {
if (!obj[prop]) {
console.log(`The property ${prop} does not exist inside object`);
}
console.log(`The value of prop is ${obj[prop]}`);
},
set: (obj, prop, newVal) => {
console.log(`The old value is ${prop[obj]}, new value is ${newVal}`);
obj[prop] = newVal;
},
});
Pros:
- Provides a way to control access to an object or its properties, enabling more fine-grained control over what clients can and cannot do with the object.
- Can help to improve performance by allowing expensive or time-consuming operations to be deferred until they are actually needed.
- Can be used to implement caching and other optimizations, improving the overall efficiency of the system.
Cons:
- Can add complexity to the codebase, making it harder to understand and reason about the behavior of the system.
- May require additional code to be written, increasing development time and potentially introducing bugs or other issues.
- May not be necessary for all use cases, and can introduce unnecessary overhead if used improperly.
3. Provider Pattern
In React projects we sometimes need to pass down data to the descendant components and we do this using Props, but there are cases when we want to pass props far down the component tree. In such a case we might end up with the long chain of passing props from one to another components called "prop drilling", which can be difficult to manage and lead to code that is hard to read and maintain. Also if we want to rename the prop in the parent component we should do the same to all props down the tree.
Instead we could use a Provider pattern and wrap every component that requires a specific prop with a Provider component, after doing this the props will be available to current and all descendant components. Basically the Provider is a HOC component which wraps our current component, and defines React.Context
, which can be imported in all child components, with React.useContext
, hence importing all available props inside child components.
the context.js file
export const AppContext = React.createContext();
the parent component
import { AppContext } from './AppContext
export const App = () => {
const data = { name: "Joe", lastName: "Doe" }
return (
<div>
<AppContext.Provider value={data}>
<SideBar />
</AppContext.Provider>
</div>
)
}
the sidebar component
import { AppContext } from './AppContext
export const SideBar = () => {
const { data } = React.useContext(AppContext)
return <span>{data.name} {data.lastName}</span>;
}
Pros:
- Centralizes state management: The Provider pattern allows you to manage state in a single location, making it easier to maintain and update state across the entire application.
- Simplifies data passing: By making global state available to child components via context, the Provider pattern eliminates the need for props drilling and other complex data passing techniques.
- Improves code readability: By encapsulating state management and providing a clear API for accessing and modifying state, the Provider pattern can make your code more readable and easier to understand.
Cons:
- Can be overkill for small applications: For smaller applications or projects with limited state management requirements, the Provider pattern may introduce unnecessary complexity and overhead.
- Requires careful design: To effectively use the Provider pattern, you need to carefully design your state management strategy and consider the implications of making state available globally.
- Can make it harder to debug: With state being managed in a centralized location, it can be harder to track down issues and debug problems when they occur.
4. Prototype Pattern
This pattern is useful to share properties among many objects of the same type. When working on a project we often need to create many objects of the same type. With ES6 syntax this is possible by implementing classes.
class Bird {
constructor(breed) {
this.breed = breed;
}
fly() {
return "Fly! " + this.breed;
}
}
const bird1 = new Bird("Ostrich");
const bird2 = new Bird("Parrot");
bird1.fly(); // Fly Ostrich!
bird2.fly(); // Fly Parrot!
Whenever we create a new Bird, the fly()
method is automatically available, because of our constructor class.
We can see the Object's prototype
by directly accessing the constructor's prototype
property or instances __proto__
property. The __proto__
is pointing to the object's constructor.
Bird.prototype === bird1.__proto__;
The prototype pattern is useful when we want to add props to objects of same type (prototype
), for example in our case all birds can fly, therefore we have the fly
prop available on the constructor, if we want to add another method to all of the birds, we could do it inside the Bird's prototype and it will be automatically available in all instances.
Bird.prototype.sayHello = () => "Hello!";
bird1.sayHello(); // Hello!
bird2.sayHello(); // Hello!
There also exists a term Prototype Chain
- In JavaScript, every object has an internal property called [[Prototype]] (also known as "dunder prototype") that points to another object, called its prototype. This forms a chain of objects that is called the prototype chain.
When you try to access a property or method of an object, JavaScript first looks for that property in the object itself. If the property is not found, JavaScript looks for it in the object's prototype, and so on up the prototype chain, until either the property is found or the end of the chain is reached (which is the Object.prototype object).
Let's create another bird, constructor, singerBird:
class SingerBird extends Bird {
constructor(props) {
super(props);
}
sing() {
return "I am singing!";
}
}
const bird3 = new SingerBird("Nightingale");
bird3.fly(); // Fly Nightingale!
bird3.sing(); // I am singing!
SingerBird.prototype; // Bird
It gets clear why it's called a prototype chain: when we try to access a property that's not directly available on the object, JavaScript recursively walks down all the objects that proto points to, until it finds the property!
Pros:
- Code Reusability: The Prototype pattern enables you to create objects with pre-defined properties and methods that can be shared and reused throughout your codebase.
- Dynamic Object Creation: With the Prototype pattern, you can create new objects dynamically by cloning an existing object, rather than having to create new objects from scratch.
- Easy to Modify and Extend: By modifying the properties and methods of an existing prototype object, you can easily modify and extend the behavior of any object created from that prototype.
Cons:
- Complexity: The Prototype pattern can add complexity to your codebase, especially when you have a large number of prototype objects or when you have deeply nested prototypes.
- Inheritance Issues: Inheritance can sometimes be difficult to manage with the Prototype pattern, especially if you have complex inheritance chains or if you need to modify the behavior of a parent prototype.
- Performance Issues: Creating objects through cloning can be slower than creating new objects from scratch, which can be an issue if you need to create a large number of objects.
5. Container/Presentational Patterns
To separate views from application logic in React we use Container/Presentational Pattern.
Let's discuss a typical React component:
import React, { useEffect, useState } from "react";
import { getDataFromServer } from "src/http";
export const PostsList = () => {
const [posts, setPosts] = useState([]);
useEffect(() => {
const getData = async () => {
const response = await getDataFromServer();
setPosts(response.data);
};
getData();
}, []);
return (
<div className="posts-list">
{posts.map(({ title, id, content, createdAt }) => (
<div className="posts-list__item" key={id}>
<div>Created At: {createdAt}</div>
<div>{title}</div>
<div>{content}</div>
</div>
))}
</div>
);
};
We want to enforce separation of concerns by dividing the component into two parts:
- Presentational Components: Components that care about how data is shown to the user. In this example, that's rendering the list of posts.
- Container Components: Components that care about what data is shown to the user. In this example, that's fetching the posts from the server
If we implement all mentioned above we will get two components:
PostsListContainer.js
import React, { useEffect, useState } from "react";
import { getDataFromServer } from "src/http";
import { PostsList } from "../component/PostsList";
export const PostsListContainer = () => {
const [posts, setPosts] = useState([]);
useEffect(() => {
const getData = async () => {
const response = await getDataFromServer();
setPosts(response.data);
};
getData();
}, []);
return <PostsList posts={posts} />;
};
PostsListComponent.js
import React from "react";
export const PostsList = ({ posts }) => {
return (
<div className="posts-list">
{
posts.map(({(title, content, createdAt)}) => (
<div className="posts-list__item">
<div>Created At: {createdAt}</div>
<div>{title}</div>
<div>{content}</div>
</div>
))
}
</div>
);
};
Alternatively we could replace the above implementation with React hooks. Introducing hooks into React made it easy to create statefull component without actually using state. Instead of creating a container in above mentioned example in order to fetch and provide data to PostsListComponent we could implement this functionality inside React hook and use it inside component.
Here is what we'll get:
useServerPosts.js (the hook)
import { useState, useEffect } from "react";
import { getDataFromServer } from "src/http";
export const useServerPosts = () => {
const [posts, setPosts] = useState([]);
useEffect(() => {
const getData = async () => {
const response = await getDataFromServer();
setPosts(response.data);
};
getData();
}, []);
return posts;
};
PostsList.js
import React from "react";
import { useServerPosts } from "../hooks/useServerPosts";
export const PostsList = () => {
const posts = useServerPosts();
return (
<div className="posts-list">
{posts.map(({(title, content, createdAt)}) => (
<div className="posts-list__item">
<div>Created At: {createdAt}</div>
<div>{title}</div>
<div>{content}</div>
</div>
))}
</div>
);
};
As we see the hook helps us to separate app logic from view. Finally we are getting almost the same result.
Pros
- The Container/Presentational pattern encourages the separation of concerns. Presentational components can be pure functions which are responsible for the UI, whereas container components are responsible for the state and data of the application. This makes it easy to enforce the separation of concerns.
- Presentational components are easily made reusable, as they simply display data without altering this data. We can reuse the presentational components throughout our application for different purposes.
- Since presentational components don't alter the application logic, the appearance of presentational components can easily be altered by someone without knowledge of the codebase, for example a designer. If the presentational component was reused in many parts of the application, the change can be consistent throughout the app.
- Testing presentational components is easy, as they are usually pure functions. We know what the components will render based on which data we pass, without having to mock a data store.
Cons
- The Container/Presentational pattern makes it easy to separate application logic from rendering logic. However, Hooks make it possible to achieve the same result without having to use the Container/Presentational pattern, and without having to rewrite a stateless functional component into a class component. Note that today, we don't need to create class components to use state anymore.
- Although we can still use the Container/Presentational pattern, even with React Hooks, this pattern can easily be an overkill in smaller sized applications.
6. Observer Pattern
different functions (observers) can be subscribed to different objects - so called observables. When some update occurs on observable all the subscribed observers get updated. Observer example:
export default class Observable {
constructor() {
this.observers = [];
}
subscribe(func) {
this.observers.push(func);
}
unsubscribe(func) {
this.observers = this.observers.filter((observer) => observer !== func);
}
notify(data) {
this.observers.forEach((observer) => observer(data));
}
}
The Observer Pattern is a design pattern in JavaScript that allows objects to subscribe to events and get notified when they occur. It is also known as the Publish-Subscribe pattern.
example:
import React, { useState } from "react";
const NotificationService = new Observable();
const ComponentRed = ({ message }) => (
<div style={{ color: "red" }}>{message}</div>
);
const ComponentGreen = ({ message }) => (
<div style={{ color: "green" }}>{message}</div>
);
const ComponentYellow = ({ message }) => (
<div style={{ color: "yellow" }}>{message}</div>
);
const ComponentOrange = ({ message }) => (
<div style={{ color: "orange" }}>{message}</div>
);
const ComponentNavy = ({ message }) => (
<div style={{ color: "navy" }}>{message}</div>
);
const MainInput = () => {
const [value, setValue] = useState("");
const handleValueChange = (val) => {
setValue(val);
NotificationService.notify(val);
};
return (
<div>
<input
value={value}
onChange={(e) => handleValueChange(e.target.value)}
/>
</div>
);
};
const Connect = (Component) => (props) => {
const [message, setMessage] = useState("");
const handleMessageChange = (msg) => {
setMessage(msg);
};
NotificationService.subscribe(handleMessageChange);
return <Component {...props} message={message} />;
};
const RedComponent = Connect(ComponentRed);
const GreenComponent = Connect(ComponentGreen);
const YellowComponent = Connect(ComponentYellow);
const OrangeComponent = Connect(ComponentOrange);
const NavyComponent = Connect(ComponentNavy);
export const Example = () => (
<>
<MainInput />
<RedComponent />
<GreenComponent />
<YellowComponent />
<OrangeComponent />
<NavyComponent />
</>
);
import React, { useEffect } from "react";
import { Example } from "./Observable";
const App = () => {
return <Example />;
};
Observer pattern is used in Redux when we link current component to Redux state using connect
function, all functions that connect to Redux state get updated when redux state changes.
Here are some of the pros and cons of using the Observer Pattern in JavaScript:
Pros
- Decoupling: The Observer Pattern promotes decoupling between objects by allowing them to communicate without being aware of each other's implementation details.
- Scalability: The Observer Pattern makes it easier to scale the application by allowing developers to add or remove observers without affecting the rest of the codebase.
- Flexibility: The Observer Pattern is flexible and can be used to implement many different types of event-driven architectures.
- Reusability: The Observer Pattern promotes code reuse by allowing developers to create reusable components that can be used in different parts of the application.
- Testability: The Observer Pattern makes it easier to test the application by providing a clear separation of concerns between different objects and their responsibilities.
Cons
- Performance impact: The Observer Pattern can have a performance impact on the application, especially when many objects are observing the same events.
- Complexity: The Observer Pattern can introduce additional complexity to the codebase, especially when managing multiple observers and their interactions.
- Tight coupling: The Observer Pattern can result in tight coupling between observers and subjects, making it harder to modify or replace them later.
- Debugging: The Observer Pattern can make it harder to debug the application by introducing multiple layers of indirection and abstraction.
- Overhead: The Observer Pattern can introduce additional overhead in the codebase, especially when using it in small-scale projects that do not require many observers or events.
In summary, the Observer Pattern is a powerful and flexible design pattern in JavaScript that allows objects to communicate with each other using events and observers. However, it also has some drawbacks, such as performance impact, complexity, tight coupling, debugging issues, and overhead. Developers should carefully consider the benefits and drawbacks of using the Observer Pattern before implementing it in their applications.
7. Module Pattern
Module pattern allows us to split code into smaller files, this is especially useful for better readability and maintainability of the code. Besides, the module pattern gives us the possibility to make a specific function private - not able to be used in other files unless it is exported from the file.
date.js:
const privateValue = "Some private value"
export default getCurrentDate = () => {
...
}
export const subtractDate = (date1, date2) => {
...
}
export const addDate = (date1, date2) => {
...
}
index.js:
import getCurrentDate, { // the default export
subtractDate,
addDate as addTwoDates,
privateValue, // Will throw an error because the `privateValue` is not exported from the `date.js` file // the named exports
} from "./date";
From the example above we see the difference between default export
(getCurrentDate) and named exports
- all other exports.
Also we saw that the privateValue
is not accessible outside the date.js
file until it is exported from the same file. As you see we also could change the name of imported function/value by using the
as
keywoard (addDate
was imported as addTwoDates
),
this is especially useful when you already have the file with the same name addDate
inside the current file.
import * from './date' // will import everything from date file, all exported functions will be availabe after doing this
getCurrentDate()
subtractDate(dt1, dt2)
addDate(dt1, dt2)
all exported functions or variables will be available inside the function after doing all import
by using *
the asterix
alternatively we could import all as date
import * as date from "./date";
// We could do
date.default(); // will call the getCurrentDate - as this is the default export
date.addDate(dt1, dt2);
date.subtractDate(dt1, dt2);
Dynamic Imports
There might be cases when we need to import a specific file/functions from a file when certain conditions are satisfied. We can use Dynamic Imports
in these case:
import("module").then((module) => {
module.default();
module.namedExport();
});
// Or with async/await
(async () => {
const module = await import("module");
module.default();
module.namedExport();
})();
Conditional Imports:
button.addEventListener("click", () => {
import("./math.js").then((module) => {
console.log("Add: ", module.add(1, 2));
console.log("Multiply: ", module.multiply(3, 2));
const button = document.getElementById("btn");
button.innerHTML = "Check the console";
});
});
or another example:
import React from "react";
export function DogImage({ num }) {
const [src, setSrc] = React.useState("");
async function loadDogImage() {
const res = await import(`../assets/dog${num}.png`);
setSrc(res.default);
}
return src ? (
<img src={src} alt="Dog" />
) : (
<div className="loader">
<button onClick={loadDogImage}>Click to load image</button>
</div>
);
}
With the module pattern, we can encapsulate parts of our code that should not be publicly exposed. This prevents accidental name collision and global scope pollution, which makes working with multiple dependencies and namespaces less risky. In order to be able to use ES2015 modules in all JavaScript runtimes, a transpiler such as Babel is needed
Pros:
- Encapsulation: The Module Pattern provides a way to encapsulate functionality and hide it from the global scope, preventing naming conflicts and making the code more organized and maintainable.
- Privacy: The Module Pattern allows developers to define private members that are inaccessible from the outside, ensuring data privacy and security.
- Reusability: The Module Pattern promotes code reuse by allowing developers to create self-contained modules that can be easily reused in different parts of the codebase.
- Scalability: The Module Pattern makes it easier to scale the application by allowing developers to create modular, decoupled components that can be easily added or removed as needed.
- Testability: The Module Pattern makes it easier to test the application by providing a clear separation of concerns between different modules and their dependencies.
Cons:
- Complexity: The Module Pattern can introduce additional complexity to the codebase, especially when managing multiple modules and their interactions.
- Overhead: The Module Pattern can introduce additional overhead in the codebase, especially when creating many small modules that need to be loaded separately.
- Inflexibility: The Module Pattern can be inflexible in some cases, especially when developers need to create modules with dynamic behavior or dependencies.
- Learning curve: The Module Pattern has a learning curve, and developers need to familiarize themselves with the syntax and conventions before using it effectively.
- Performance impact: The Module Pattern can have a performance impact on the application, especially when using it in large-scale projects that require many modules and dependencies.
8. Mixin Pattern
Mixin is an object that we can use to implement reusable functionality to extend other object's functionalities without using inheritance.
Example:
class Dog {
constructor(name) {
this.name = name;
}
}
const animalFunctionality = {
walk: () => console.log("Walking!"),
sleep: () => console.log("Sleeping!"),
};
const dogFunctionality = {
__proto__: animalFunctionality,
bark: () => console.log("Woof!"),
wagTail: () => console.log("Wagging my tail!"),
play: () => console.log("Playing!"),
walk() {
super.walk();
},
sleep() {
super.sleep();
},
};
Object.assign(Dog.prototype, dogFunctionality);
const pet1 = new Dog("Daisy");
console.log(pet1.name);
pet1.bark();
pet1.play();
pet1.walk();
pet1.sleep();
Mixins were often used to add functionality to React components before the introduction of ES6 classes. The React team discourages the use of mixins as it easily adds unnecessary complexity to a component, making it hard to maintain and reuse. The React team encouraged the use of higher order components instead, which can now often be replaced by Hooks.
Mixins allow us to easily add functionality to objects without inheritance by injecting functionality into an object's prototype. Modifying an object's prototype is seen as bad practice, as it can lead to prototype pollution and a level of uncertainty regarding the origin of our functions.
The Mixin pattern in JavaScript is a design pattern that allows objects to inherit properties and methods from multiple sources, rather than just a single parent object. The Mixin pattern involves creating a reusable function that contains the shared functionality, and then mixing it into the objects that need it. Here are some benefits and drawbacks of using the Mixin pattern in JavaScript:
Pros:
- Code reuse: The Mixin pattern promotes code reuse by allowing objects to inherit functionality from multiple sources.
- Flexibility: The Mixin pattern allows objects to inherit only the functionality they need, making the code more flexible and modular.
- Encapsulation: The Mixin pattern can help to encapsulate shared functionality, reducing the likelihood of bugs and making the codebase easier to maintain.
- Compatibility: The Mixin pattern is compatible with many other design patterns and can be used to enhance their functionality.
- Readability: The Mixin pattern can improve the readability of the code by separating shared functionality from object-specific functionality.
Cons:
- Naming collisions: The Mixin pattern can result in naming collisions when different mixins define the same property or method.
- Complexity: The Mixin pattern can introduce additional complexity to the codebase, especially when managing multiple mixins and their interactions.
- Tight coupling: The Mixin pattern can result in tight coupling between objects and mixins, making it harder to modify or replace them later.
- Dependency management: The Mixin pattern can introduce additional dependencies in the codebase, which may impact performance and require additional maintenance.
- Debugging: The Mixin pattern can make it harder to debug the codebase by introducing shared functionality that is used by multiple objects.
In summary, the Mixin pattern is a powerful and flexible design pattern in JavaScript that allows objects to inherit functionality from multiple sources. However, it also has some drawbacks, such as naming collisions, complexity, tight coupling, dependency management, and debugging issues. Developers should carefully consider the benefits and drawbacks of using the Mixin pattern before implementing it in their applications.
9. Mediator/Middleware Pattern
The Mediator/Middleware pattern is a design pattern in JavaScript that allows components to communicate with each other without knowing about each other directly. Instead, they communicate through a central mediator or middleware that handles the communication between them. Expressjs and Redux libraries are widely using Mediator/Middleware pattern.
Redux uses the Mediator/Middleware pattern to manage communication between different parts of the application, such as actions and reducers, by using a single store to hold the application state. This promotes loose coupling between these different parts and allows for more maintainable and scalable applications.
In Redux, a middleware is a function that intercepts and handles actions before they reach the reducers. This middleware acts as a mediator between the action and the reducer, allowing additional processing to occur before the state is updated.
Here's an example of a simple middleware that logs all actions:
const loggerMiddleware = (store) => (next) => (action) => {
console.log("Action:", action);
next(action);
};
In this example, we define a middleware function that takes the store as an argument and returns a function that takes the next middleware as an argument and returns a function that takes the action as an argument. This function logs the action and then calls the next middleware in the chain by invoking the next function.
To use this middleware in a Redux application, we would include it when creating the store (in this example we implement ReduxThunk
and loggerMiddleware
):
import { createStore, applyMiddleware } from "redux";
import loggerMiddleware from "./loggerMiddleware";
import rootReducer from "./reducers";
const store = createStore(
rootReducer,
applyMiddleware(ReduxThunk, loggerMiddleware)
);
Here are some of the benefits and drawbacks of using the Mediator/Middleware pattern in JavaScript:
Pros:
- Decoupling: The Mediator/Middleware pattern promotes loose coupling between components by eliminating direct references between them. This can make the code more flexible, maintainable, and testable.
- Centralized control: The Mediator/Middleware pattern provides a centralized control point for communication between components, which can simplify the codebase and reduce the likelihood of bugs.
- Reusability: The Mediator/Middleware pattern allows developers to reuse the same mediator or middleware across different components, which can reduce code duplication and improve the maintainability of the codebase.
- Scalability: The Mediator/Middleware pattern can make it easier to scale the application by allowing new components to be added or removed without affecting the rest of the codebase.
- Debugging: The Mediator/Middleware pattern can make it easier to debug the application by providing a single point of entry for monitoring and tracing communication between components.
Cons:
- Complexity: The Mediator/Middleware pattern can introduce additional complexity to the codebase, especially when managing multiple components and interactions between them.
- Learning curve: The Mediator/Middleware pattern has a learning curve, and developers need to familiarize themselves with the new syntax and conventions.
- Performance overhead: The Mediator/Middleware pattern can introduce performance overhead, especially when the mediator or middleware needs to handle a large number of interactions between components.
- Dependency management: The Mediator/Middleware pattern can introduce additional dependencies in the codebase, which may impact performance and require additional maintenance.
- Limitations: The Mediator/Middleware pattern has certain limitations, such as not being able to handle complex or dynamic interactions between components.
In summary, the Mediator/Middleware pattern is a powerful and flexible design pattern in JavaScript that allows components to communicate with each other without knowing about each other directly. However, it also has some drawbacks, such as complexity, performance overhead, and limitations. Developers should carefully consider the benefits and drawbacks of using the Mediator/Middleware pattern before implementing it in their applications.
10. HOC Pattern
The Higher Order Component (HOC) pattern is a design pattern in JavaScript that allows developers to reuse component logic across multiple components in a React application. HOCs are functions that take a component as an argument and return a new component with additional functionality.
Here is am example:
function withStyles(Component) {
return props => {
const style = { padding: '0.2rem', margin: '1rem' }
return <Component style={style} {...props} />
}
}
const Button = () = <button>Click me!</button>
const Text = () => <p>Hello World!</p>
const StyledButton = withStyles(Button)
const StyledText = withStyles(Text)
The HOC component is frequently used when we want to display loading indicator for a specific component:
withLoader.js
export default function withLoader(Element, url) {
return (props) => {
const [data, setData] = useState(null);
useEffect(() => {
async function getData() {
const res = await fetch(url);
const data = await res.json();
setData(data);
}
getData();
}, []);
if (!data) {
return <div>Loading...</div>;
}
return <Element {...props} data={data} />;
};
}
postsList.js
import 'withLoader' from './withLoader'
function PostsList(props) {
return props.data.message.map((post) => (
<div key={post.id}>
<div>{post.title}</div>
<div>{post.author}</div>
<div>{post.content}</div>
</div>
));
}
export default withLoader(PostsList, 'https://www.example.com/posts')
after wrapping the default export with withLoader, we are implementing the Loading...
functionality for the PostsList component. We can easily distribute the same functionality to other components as well, by wrapping them in HOC.
We could chain the HOC components, for example, the Redux Library exports a connect
HOC function, which can be useed to connect to the redux state, finally we might end up with the following:
connect(mapStateToProps, mapDispatchToProps)(withLoader(PostsList, 'https://www.example.com/posts'))
The benefits that HOC pattern provides, can be implemented with React Hooks. After introducing React Hooks, more HOCs were replaced with Hooks. However this does not mean that Hooks completely replace the HOC pattern, just that they might be used interchangeably. One benefit of using Hooks over HOC, is that when chaining multiple HOCs it might happen that we might end up deeply nesting all components, which is not good for readability.
Best use-cases for a HOC:
The same, uncustomized behavior needs to be used by many components throughout the application. The component can work standalone, without the added custom logic.
Best use-cases for Hooks:
The behavior has to be customized for each component that uses it. The behavior is not spread throughout the application, only one or a few components use the behavior. The behavior adds many properties to the component
Pros
- Reusability: The HOC pattern allows developers to reuse component logic across multiple components in a React application, which can reduce code duplication and improve the maintainability of the codebase.
- Improved readability: HOCs can improve the readability of React code by making it easier to understand how components are composed and what functionality they provide.
- Simplified codebase: HOCs can simplify the codebase by removing the need to repeat logic in multiple components.
- Improved testability: HOCs can improve the testability of React code by allowing developers to test component logic in isolation.
- Improved developer experience: HOCs can improve the developer experience by providing a simpler and more consistent way to compose components.
Cons
- Learning curve: The HOC pattern has a learning curve, and developers need to familiarize themselves with the new syntax and conventions.
- Compatibility: HOCs can introduce compatibility issues with other components, especially when using third-party libraries.
- Performance overhead: HOCs can introduce performance overhead, especially when the HOCs are composed or nested.
- Complexity: HOCs can introduce additional complexity to the codebase, especially when managing multiple HOCs in a component.
- Limitations: HOCs have certain limitations, such as not being able to pass props to the wrapped component during initialization or lifecycle methods.
11. Render Props Pattern
Render props is a design pattern in React that allows for sharing code between components by passing a function as a prop. The function is used by the receiving component to render its content, and the passed data can be manipulated and modified by the receiving component before it's rendered.
The render props pattern is a powerful technique that provides a lot of flexibility for building reusable components in React. It can be used to separate the logic from the presentation, making it easier to maintain and test individual components. Additionally, the pattern promotes composability, which allows for building complex user interfaces from simple building blocks.
Lets see an example:
const Title = (props) => props.render();
<Title render={() => <h1>I am a render prop!</h1>} />
<Title render={() => <h2>I am a render prop inside H2 Element!</h2>} />
<Title render={() => <div>I am a render prop inside DIV Element!</div>} />
As we see we are rendering 3 different components for each Title. Note that we are naming the function as render
, however this is not a requirement, we can call this function however we would like to call it (for example in the current example we could call it display
, showTitle
...).
let's discuss the following scenario:
function Input({ value, handleChange }) {
return <input value={value} onChange={(e) => handleChange(e.target.value)} />;
}
export default function App() {
const [value, setValue] = useState("");
return (
<div className="App">
<h1>☃️ Temperature Converter 🌞</h1>
<Input value={value} handleChange={setValue} />
<Kelvin value={value} />
<Fahrenheit value={value} />
</div>
);
}
we could represent this with Render Props Pattern
in the following way:
function Input(props) {
const [value, setValue] = useState("");
return (
<>
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Temp in °C"
/>
{props.render(value)}
</>
);
}
export default function App() {
return (
<div className="App">
<h1>☃️ Temperature Converter 🌞</h1>
<Input
render={(value) => (
<>
<Kelvin value={value} />
<Fahrenheit value={value} />
</>
)}
/>
</div>
);
}
Besides regular JSX components, we can pass functions as children to React components. This function is available to us through the children prop, which is technically also a render prop.
Let's change the Input component. Instead of explicitly passing the render prop, we'll just pass a function as a child for the Input component.
Example:
export default function App() {
return (
<div className="App">
<h1>☃️ Temperature Converter 🌞</h1>
<Input>
{(value) => (
<>
<Kelvin value={value} />
<Fahrenheit value={value} />
</>
)}
</Input>
</div>
);
}
Pros
- Sharing logic and data among several components is easy with the render props pattern. Components can be made very reusable, by using a render or children prop. Although the Higher Order Component pattern mainly solves the same issues, namely reusability and sharing data, the render props pattern solves some of the issues we could encounter by using the HOC pattern.
- The issue of naming collisions that we can run into by using the HOC pattern no longer applies by using the render props pattern, since we don't automatically merge props. We explicitly pass the props down to the child components, with the value provided by the parent component.
- Since we explicitly pass props, we solve the HOC's implicit props issue. The props that should get passed down to the element, are all visible in the render prop's arguments list. This way, we know exactly where certain props come from.
- We can separate our app's logic from rendering components through render props. The stateful component that receives a render prop can pass the data onto stateless components, which merely render the data.
Cons
- The issues that we tried to solve with render props, have largely been replaced by React Hooks. As Hooks changed the way we can add reusability and data sharing to components, they can replace the render props pattern in many cases.
- Since we can't add lifecycle methods to a render prop, we can only use it on components that don't need to alter the data they receive.
12. Hooks Pattern
The Hooks pattern is a design pattern in React. The Hooks pattern allows developers to reuse stateful logic across multiple components in a React application. Hooks are functions that allow developers to use state and other React features without writing a class. Here are some of the benefits and drawbacks of using the Hooks pattern in JavaScript:
for example: useAsync.js
import React, { ueState, useEffect } from "react";
export const useAsync = (fn, val) => {
const [dataObj, setDataObj] = useState({
value,
loading,
error,
});
useEffect(() => {
const getData = async () => {
setDataObj({
...dataObj,
loading: true,
});
try {
const value = await fn();
setDataObj({
...dataObj,
loading: false,
value,
});
} catch (error) {
setDataObj({
...dataObj,
loading: false
error,
});
}
};
getData();
}, [fn, val]);
return dataObj
};
posts.js
import React from "react";
import { useAsyn } from "..hooks/useAsync";
import { Progress, WarningPanel } from "../components/shared";
export const Posts = () => {
const {
value: posts,
loading,
error,
} = useAsync(mainApi.getPosts
}, [mainApi]);
if (loading) {
return <Progress />;
}
if (error) {
return (
<WarningPanel title="Failed to load posts" description={error.message} />
);
}
return posts.map(({id, title, createdAt, author}) => (
<div key={id}>
<div>{title}</div>
<div>{createdAt}</div>
<div>{author}</div>
</div>
))
};
Pros:
- Reusability: The Hooks pattern allows developers to reuse stateful logic across multiple components in a React application, which can reduce code duplication and improve the maintainability of the codebase.
- Improved readability: Hooks can improve the readability of React code by making it easier to understand how state is managed and shared between components.
- Simplified codebase: Hooks can simplify the codebase by removing the need for class components and lifecycle methods.
- Improved testability: Hooks can improve the testability of React code by allowing developers to test stateful logic in isolation.
- Improved developer experience: Hooks can improve the developer experience by providing a simpler and more consistent way to manage state and other React features.
Cons:
- Learning curve: The Hooks pattern has a learning curve, and developers need to familiarize themselves with the new syntax and conventions.
- Compatibility: The Hooks pattern is only available in newer versions of React, which may not be compatible with older codebases.
- Dependency management: Hooks can introduce additional dependencies in the codebase, which may impact performance and require additional maintenance.
- Complexity: Hooks can introduce additional complexity to the codebase, especially when managing multiple stateful hooks in a component.
- Limitations: Hooks have certain limitations, such as not being able to use them in class components or outside of React components.
In summary, the Hooks pattern is a powerful and popular design pattern in React that allows developers to reuse stateful logic and simplify their codebase. However, it also has some drawbacks, such as a learning curve, dependency management, and additional complexity. Developers should carefully consider the benefits and drawbacks of using the Hooks pattern before implementing it in their React applications.
13. Flyweight Pattern
The Flyweight pattern is a software design pattern that is used to minimize memory usage and improve performance by sharing as much data as possible between similar objects. In the context of JavaScript, the Flyweight pattern can be used to optimize the creation and management of objects that have similar properties. For example let's imagine we have different types of chairs and we may have multiple copies of a single type of chair (For example Office Chair). In order to create a chairs list we need to create a constructor first:
class Chair {
constructor(name, creator, type) {
this.name = name;
this.creator = creator;
this.type = type;
}
}
const chairs = new Map();
const createChair = (name, creator, type) => {
const existingChair = chairs.has(type);
if (existingChair) {
return chairs.get(type);
}
// If the chair does not exists
const chair = new Chair(name, creator, type);
chairs.set(type, chair);
return chair;
};
In the above example we have a map of chairs, and a createChair
function, which checks if the chair exists in a map and returns it, otherwise it creates a new Chair.
Now let's create a chairStore functionality to keep track of all chairs:
const chairStore = [];
const addChair = (name, creator, type) => {
const chair = createChair(name, creator, type);
chairStore.push(chair);
return chair;
};
In the above example, instead of creating new instances of the same types of chair, we simply return a reference to the chair type instance that already exists, otherwise we return a new instance.
addChair("Office Chair", "John Doe", "OFFICE_CHAIR");
addChair("Office Chair", "John Doe", "OFFICE_CHAIR");
addChair("Racking Chair", "Jane Doe", "RACKING_CHAIR");
addChair("Racking Chair", "Jane Doe", "RACKING_CHAIR");
addChair("Deck Chair", "John Doe", "DECK_CHAIR");
In this example we created 5 chairs but actually 3 instances of these chairs were created. The flyweight pattern is useful when you're creating a huge number of objects, which could potentially drain all available RAM. It allows us to minimize the amount of consumed memory.
Pros
- Reduced memory usage: The Flyweight pattern reduces memory usage by sharing data among multiple objects, rather than storing duplicate data in each object.
- Improved performance: The Flyweight pattern can improve performance by reducing the number of objects created and increasing the efficiency of object creation.
- Enhanced scalability: The Flyweight pattern enhances the scalability of an application by reducing the memory usage, which allows for the creation of a large number of objects without impacting the performance of the application.
- Simplified codebase: The Flyweight pattern simplifies the codebase by separating the intrinsic and extrinsic data of an object, which makes it easier to maintain and modify the codebase.
- Improved code readability: The Flyweight pattern can improve code readability by making it easier to understand how objects are created and shared in the application.
Cons
- Increased complexity: The Flyweight pattern can increase the complexity of an application, especially when dealing with complex data structures and sharing logic.
- Potential for reduced security: The Flyweight pattern can potentially reduce security, as the shared data may be accessible to unintended parties.
- Increased maintenance cost: The Flyweight pattern can increase the maintenance cost of an application, as any changes to the shared data may require updates to multiple objects.
- Reduced flexibility: The Flyweight pattern can reduce the flexibility of an application, as it may limit the ability to modify objects dynamically.
- Increased implementation time: The Flyweight pattern may increase the implementation time of an application, as it requires careful consideration of the data structures and sharing logic to be used.
14. Factory Pattern
We can create new objects without using the new
keyword. Functions that return an object are called factory functions
.
The factory pattern can be useful if we're creating relatively complex and configurable objects. It could happen that the values of the keys and values are dependent on a certain environment or configuration. With the factory pattern, we can easily create new objects that contain the custom keys and values!
Example:
const createUser = ({ firstName, lastName, email }) => ({
firstName,
lastName,
email,
fullName() {
return `${this.firstName} ${this.lastName}`;
},
});
const user1 = createUser({
firstName: "John",
lastName: "Doe",
email: "john@example.com",
});
const user2 = createUser({
firstName: "Jane",
lastName: "Doe",
email: "jane@example.com",
});
Pros
- Encapsulates object creation: The Factory pattern encapsulates object creation logic, making the code more modular and reusable.
- Decouples client code: The Factory pattern decouples the client code from the object creation process, allowing changes in one without affecting the other.
- Enhances flexibility: The Factory pattern makes it easier to change the object creation process without affecting the client code. This enhances the flexibility of the codebase and makes it easier to maintain.
- Improves code readability: The Factory pattern can improve code readability by providing a clear separation between the object creation process and the client code.
Cons
- Increased complexity: The Factory pattern can introduce additional complexity to the codebase, especially when creating complex objects with multiple parameters.
- Reduced performance: The Factory pattern can be slower than direct object creation, as it involves additional function calls and logic.
- Increased memory usage: The Factory pattern can increase memory usage, as it requires the creation of additional objects and functions.
15. Compound Pattern
Compound Pattern allows us to create components that all work together to perform a shared task, these components depend on each other and that's why they share a state.
Material-UI library uses Compound Pattern to create reusable components, such as DropDown
, MenuItems
, ListItems
....
Sharing state between these Compound Components is done using React Context.
Let's see an example:
MainMenu.js
import React, { useState, createContext } from "react";
const EditableFieldsetContext = createContext();
const EditableFieldset = ({ children }) => {
const [editMode, setEditMode] = useState(false);
const toggleEditMode = () => setEditMode(!editMode);
return (
<EditableFieldsetContext.Provider value={{ editMode, toggleEditMode }}>
{children}
</EditableFieldsetContext.Provider>
);
};
const Toggle = () => {};
const FieldSet = ({ value, onChange, ...rest }) => {
const { editMode } = useContext(EditableFieldsetContext);
return editMode ? (
<input value={value} onChange={onChange} {...rest} />
) : (
<p>{value}</p>
);
};
const Footer = () => {
const { open } = useContext(FlyOutContext);
return open && <div className="main-manu__footer">Footer text!</div>;
};
MainMenu.Toggle = Toggle;
MainMenu.List = List;
MainMenu.ListItem = ListItem;
MainMenu.Footer = Footer;
ListItems
import React from "react";
import { MainMenu } from "./MainMenu";
export default function MainPage({items}) {
return (
<MainMenu>
<MainMenu.Toggle />
<MainMenu.List>
<MainMenu.ListItem item={{title: "MenuItem #1", link: 'https://google.com'}}/>
<MainMenu.ListItem item={{title: "MenuItem #2", link: 'https://example.com'}}/>
<MainMenu.List>
<MainMenu.Footer />
</MainMenu>
);
}
Instead of React.Context
we could use React.Children.map and clone all children elementes and adding open
and toggle
props, here is how we can do it:
import React, { useState, Children, cloneElement } from "react";
const MainMenu = (props) => {
const [open, toggle] = useState(false);
return (
<div>
{Children.map(props.children, (child) =>
cloneElement(child, { open, toggle })
)}
</div>
);
};
Pros
- Compound components manage their own internal state, which they share among the several child components.
- When implementing a compound component, we don't have to worry about managing the state ourselves.
- When importing a compound component, we don't have to explicitly import the child components that are available on that component.
Cons
- When using the React.Children.map to provide the values, the component nesting is limited. Only direct children of the parent component will have access to the open and toggle props, meaning we can't wrap any of these components in another component.
- Cloning an element with React.cloneElement performs a shallow merge. Already existing props will be merged together with the new props that we pass. This could end up in a naming collision, if an already existing prop has the same name as the props we're passing to the React.cloneElement method. As the props are shallowly merged, the value of that prop will be overwritten with the latest value that we pass
16. Command Pattern
Sometimes we do not want to use object methods inside code, because there is a chance that these methods get renamed frequently. For this type of situation we can use Command Pattern: In JavaScript, the Command pattern is commonly used for implementing undo/redo functionality, or for implementing complex UI interactions where the user's actions need to be translated into discrete, reusable commands.
Let's say we have a construction employee class:
class Employee {
constructor(name) {
this.tools = [];
this.name = name;
}
addElectricalTool(tool) {
this.tools.push(tool);
console.log(`New tool ${tool} was added to ${name} toolbox`);
}
removeElectricalTool(tool) {
this.tools = this.tools.filter((item) => item !== tool);
console.log(`Tool with id ${tool} was removed from employee tools`);
}
}
If we want to use the Employee class methods we would do the following:
const newEmp = new Employee("John Doe");
newEmp.addElectricalTool("Drill");
Now let's imagine that these class methods are renamed, for example the addElectricalTool
was renamed to addElectricalEquipment
, which can be simply implemented by swapping those names, however if the application is too big and complex this could cause a huge problems. A better solution would be to decouple these methods from the Employee class.
we would get the following:
class ToolManager {
constructor() {
this.tools = [];
}
execute(command, ...args) {
return command.execute(this.tools, ...args);
}
}
class Command {
constructor(execute) {
this.execute = execute;
}
}
function addElectricalTool(tool) {
return new Command((tools) => {
tools.push(tool);
return `You have successfully added ${tool}`;
});
}
function removeElectricalTool(tool) {
return new Command((tools) => {
tools = tools.filter((tl) => tl !== tool);
return `You have successfully removed ${tool}`;
});
}
Now these methods are decoupled from main class, and even if we decide to rename these functions we will not have any issues, because the changes will be done at single place.
Pros
- Decoupling: The Command pattern helps decouple the object that issues a request from the objects that perform the request. This can make your code more flexible and easier to maintain.
- Reusability: By encapsulating commands as objects, you can easily reuse and compose commands in different ways to achieve different behavior.
- Undo/Redo: The Command pattern is well-suited for implementing undo/redo functionality, where you can store a history of commands that can be undone or redone.
Cons
- Complexity: The Command pattern can add some complexity to your codebase, especially when you have a large number of commands or when you need to manage the execution order of multiple commands.
- Performance: Depending on the complexity of your commands and the number of commands you need to execute, the Command pattern can sometimes impact performance, especially in performance-critical applications.
- Overengineering: In some cases, the Command pattern can be overengineered and may not be the simplest or most straightforward way to achieve the desired behavior.
Recap
In conclusion, JavaScript design patterns are an essential tool for any JavaScript developer. They provide a way to structure and organize code to make it more efficient, maintainable, and scalable. By understanding and implementing these patterns, you can improve the quality of your code and make it more reusable and easier to maintain.