Design Patterns Widely Used by JavaScript & React Developers

Oguzhan Yuksel
ÇSTech
Published in
5 min readNov 8, 2023

Design patterns are crucial for software developers and architects, offering proven solutions to common development challenges. They provide reusable blueprints, promoting best practices and code organization, akin to tried-and-true recipes.

Photo by Adrien Olichon on Unsplash

In this article, we’ll explore design patterns in JavaScript and React, offering explanations and practical examples. Whether you’re new to design patterns or looking to enhance your skills, this guide will help you apply these invaluable concepts to your projects and improve your software development endeavors.

Singleton Pattern:

The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. This is useful for scenarios where you want to maintain a single point of control, such as a configuration manager or a shared resource.

class Singleton {
constructor() {
if (Singleton.instance) {
return Singleton.instance;
}
Singleton.instance = this;
}

someMethod() {
// Your code here
}
}

const instance1 = new Singleton();
const instance2 = new Singleton();

console.log(instance1 === instance2); // true

Singleton can only have one instance, and any subsequent attempts to create new instances will return the original instance.

Proxy Pattern:

The Proxy pattern provides a surrogate or placeholder for another object to control access to it. It can be used for various purposes, such as lazy loading of resources or access control.

class RealImage {
display() {
console.log("Displaying the real image.");
}
}

class ProxyImage {
constructor() {
this.realImage = new RealImage();
}

display() {
console.log("Loading the image...");
this.realImage.display();
}
}

const image = new ProxyImage();
image.display();

ProxyImage serves as a proxy for the RealImage. It controls access to the RealImage and can perform additional tasks, such as loading the image before displaying it.

Factory Pattern:

The Factory pattern is a creational pattern that abstracts the process of object creation, allowing the client code to be decoupled from the specific classes being instantiated.

class Car {
constructor(make, model) {
this.make = make;
this.model = model;
}
}

class Bicycle {
constructor(type) {
this.type = type;
}
}

class VehicleFactory {
createVehicle(type, ...args) {
if (type === 'car') {
return new Car(...args);
} else if (type === 'bicycle') {
return new Bicycle(...args);
} else {
throw new Error("Unsupported vehicle type");
}
}
}

const factory = new VehicleFactory();
const car1 = factory.createVehicle('car', 'Toyota', 'Camry');
const bicycle1 = factory.createVehicle('bicycle', 'Mountain');

console.log(car1); // Car { make: 'Toyota', model: 'Camry' }
console.log(bicycle1); // Bicycle { type: 'Mountain' }

The VehicleFactory abstracts the creation of Car or Bicycleobjects. This separation allows you to create different types of vehicles or change the creation logic without modifying the client code.

This class-based Factory Pattern still provides the abstraction for creating objects and allows you to maintain a consistent interface for object creation while using modern ES6 class syntax.

Observer Pattern:

The Observer pattern defines a one-to-many relationship between objects, so when one object changes state, all its dependents are notified and updated automatically.

class Subject {
constructor() {
this.observers = [];
}

addObserver(observer) {
this.observers.push(observer);
}

notify(message) {
this.observers.forEach(observer => observer.update(message));
}
}

class Observer {
update(message) {
console.log(`Received message: ${message}`);
}
}

const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();

subject.addObserver(observer1);
subject.addObserver(observer2);

subject.notify("Hello, observers!");

The Subject maintains a list of observers, and when it changes state (in this case, sends a message), all registered observers are notified.

This is the end of the topics about Javascript Design Patterns. Now let’s move on to the React Design Patterns which are more widely used nowadays.

React Design Patterns

Container / Presentational Pattern

The Container/Presentational pattern separates the logic (Container components) from the rendering (Presentational components). Container components handle data and state management, while Presentational components focus on rendering.

// Container Arrow Function Component
import React, { useState } from 'react';
import Presentation from './Presentation';

const Container = () => {
const [data, setData] = useState([]);

// Logic and state management

return <Presentation data={data} />;
};

export default Container; // Export the Container component

// Presentational Component
const Presentation = ({ data }) => {
// Rendering logic using data
};

export default Presentation; // Export the Presentation component

Container components like Container handle data fetching and state management, while Presentational components like Presentation focus solely on rendering.

Higher-Order Component (HOC) Pattern:

A Higher-Order Component (HOC) is a function that takes a component and returns a new component with added behavior or props.

import React from 'react';

const withLogger = WrappedComponent => {
return class ComponentWithLogger extends React.Component {
componentDidMount() {
console.log(`Component ${WrappedComponent.name} has mounted.`);
}

render() {
return <WrappedComponent {...this.props} />;
}
};
};

const MyComponent = () => <div>My Component</div>;
const MyComponentWithLogger = withLogger(MyComponent);

Here, withLogger is a HOC that logs when a component is mounted. It enhances the MyComponent with additional behavior.

While HOCs were initially associated with class components, you can create and use HOCs with functional components using React hooks. In order not to make it too long, I will not give an example of functional component usage separately.

Provider & Compound Pattern:

The Provider pattern, frequently utilized in conjunction with the Context API, is a powerful tool for managing shared data in React. It allows you to pass data or functionality down the component tree without the need for explicit prop drilling.

import React, { createContext, useContext } from 'react';

const MyContext = createContext();

function MyProvider({ children }) {
const value = { data: 'Some context data' };
return <MyContext.Provider value={value}>{children}</MyContext.Provider>;
}

function ConsumerComponent() {
const value = useContext(MyContext);
return <div>{value.data}</div>;
}

The MyProvider component wraps its children with a context that provides shared data. Nested components like ConsumerComponent can easily access this data without the need for passing it explicitly through props. The Provider pattern simplifies data management and reduces the complexity of component communication.

There are many more design patterns available. You can use these examples as a starting point to discuss each design pattern’s purpose, benefits, and how they can be applied in JavaScript and React applications.

PS: In the React world, Functional components have become increasingly popular, and they offer a clean and concise way to implement these patterns in modern React applications. Being eye familiar with those would help to understand and write better code.

Until next time, take care! 👋

Additional Part

You can reach me through the channels below,

LinkedIn: https://www.linkedin.com/in/developeroguzyuksel/

GitHub: https://github.com/ooguzyuksel

Oğuzhan YÜKSEL

Frontend Developer ÇSTech

--

--