Understanding Design Patterns in React and JavaScript

Leul Ayalew
5 min readMar 26, 2023

--

Photo by Andrew Ridley on Unsplash

Introduction

Design patterns are reusable solutions to commonly occurring problems in software engineering. They help to write maintainable and scalable code. In this blog post, we will explore some of the popular design patterns in React and JavaScript.

Singleton Pattern

The Singleton pattern is used to ensure that only one instance of a class is created. It is useful when we want to restrict the number of instances of a class to only one. In JavaScript, we can implement the Singleton pattern using a closure.

Here is an example code snippet:

const Singleton = (function() {
let instance;
function createInstance() {
// singleton code here
return {};
}
return {
getInstance: function() {
if (!instance) {
instance = createInstance();
}
return instance;
}
}
})();

In this code snippet, we create a Singleton object using a closure. The getInstance() method is used to get the instance of the Singleton object. If the instance does not exist, it creates a new one using the createInstance() method.

Observer Pattern

The Observer pattern is used when there is a one-to-many relationship between objects. When one object changes its state, all the dependent objects are notified and updated automatically. In React, we can use the Observer pattern to manage the state of components. We can use the useState() hook to create state variables and pass them as props to child components. When the state changes, React automatically re-renders the component and its children.

Here is an example code snippet:

import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const handleClick = ()=> {
setCount(pre => pre + 1);
}
return (
<div>
<p>{count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}

In this code snippet, we create a Counter component that uses the useState() hook to manage the count state variable. When the user clicks the Increment button, the handleClick() function is called, which updates the count state variable. React automatically re-renders the component and updates the view.

Factory Pattern

The Factory pattern is used to create objects without exposing the object creation logic to the client. It provides a way to delegate the object creation to a factory object. In JavaScript, we can implement the Factory pattern using a function that returns a new object. Here is an example code snippet:

function createPerson(name, age) {
return {
name,
age,
greet: function() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
}
}
const person1 = createPerson('John', 30);
person1.greet();

In this code snippet, we create a createPerson() function that returns a new person object. The person1 object is created using the createPerson() function, which takes the name and age as parameters.

State Pattern

The State pattern is used when an object’s behavior depends on its state and can change dynamically. In React, we can use the State pattern to manage the state of components. We can create different states for different scenarios and change the state of the component based on user interaction. Here is an example code snippet:

import { useState } from 'react';
function LoginForm() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [loggedIn, setLoggedIn] = useState(false);
function handleSubmit(event) {
event.preventDefault();
// login logic here
setLoggedIn(true);
}
if (loggedIn) {
return <p>Welcome {username}!</p>;
}
return (
<form onSubmit={handleSubmit}>
<input type="text" value={username} onChange={(event) => setUsername(event.target.value)} />
<input type="password" value={password} onChange={(event) => setPassword(event.target.value)} />
<button type="submit">Login</button>
</form>
);
}

In this code snippet, we create a LoginForm component that manages the state of the component using the useState() hook. The component has three states: username, password, and loggedIn. The handleSubmit() function is called when the user submits the form. It checks the username and password and sets the loggedIn state to true if the login is successful. The component renders differently based on the loggedIn state.

Strategy Pattern

The Strategy pattern is used when we want to encapsulate different algorithms and make them interchangeable. In JavaScript, we can use the Strategy pattern to create different functions and pass them as parameters to other functions. Here is an example code snippet:

function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
function calculate(a, b, operation) {
return operation(a, b);
}
console.log(calculate(10, 5, add)); // 15
console.log(calculate(10, 5, subtract)); // 5

In this code snippet, we create two functions add() and subtract() that perform different operations. We create a calculate() function that takes two numbers and an operation as parameters and returns the result of the operation. We can pass different operations to the calculate() function and get different results.

Adapter Pattern

The Adapter pattern is used to convert the interface of a class to another interface that the client expects. It is useful when two different systems have incompatible interfaces. In React, we can use the Adapter pattern to wrap a component and provide a different interface. Here is an example code snippet:

import { useState } from 'react';

function Counter({ initialValue }) {
const [count, setCount] = useState(initialValue);
function increment() {
setCount(count + 1);
}
function decrement() {
setCount(count - 1);
}
return (
<div>
<p>{count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
}
function CounterAdapter({ initialValue }) {
const [count, setCount] = useState(initialValue);
function increment() {
setCount(count + 1);
}
function decrement() {
setCount(count - 1);
}
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Add 1</button>
<button onClick={decrement}>Subtract 1</button>
</div>
);
}
function App() {
return (
<>
<Counter initialValue={0} />
<CounterAdapter initialValue={0} />
</>
);
}

In this code snippet, we create a Counter component that manages the state of the component using the useState() hook. We also create a CounterAdapter component that wraps the Counter component and provides a different interface. The App component renders both components and demonstrates the use of the Adapter pattern.

Conclusion

Design patterns are an essential tool in software engineering. They help to write maintainable and scalable code. In this blog post, we explored some of the popular design patterns in React and JavaScript. We discussed the Singleton, Observer, Factory, State, Strategy, and Adapter patterns and provided some code examples. By using these design patterns, we can create robust and scalable applications. Understanding and applying these design patterns can make our code more maintainable and reusable.

Leul Ayalew

Software Engineer

--

--

Leul Ayalew

I'm a Software Engineer, passionate about building beautiful and performant web and mobile applications.