Understanding Design Patterns in React and JavaScript
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