16 Design Patterns for Javascript Developers

September 10, 2023

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?

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:

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:

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:


Cons:


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.

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:


Cons:


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:


Cons:


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:


Cons:


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:

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


Cons


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


Cons


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:


Cons:


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:


Cons:


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:

Cons:

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


Cons


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


Cons


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:


Cons:


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


Cons


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


Cons


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


Cons


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


Cons


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.

References

About the author: Shalva Gelenidze
Comments
Join us