4

Using GoF Design Patterns with React

 1 year ago
source link: https://blog.bitsrc.io/using-gof-design-patterns-with-react-c334f3ea3147
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Using GoF Design Patterns with React

How to write reusable code in React using three Gang-of-Four Design Patterns

0*8ExB5x6-clXENLsm.jpeg

Design patterns provide reusable and well-proven solutions for common issues in software development. Using a design pattern can save countless hours of development and help deliver the feature to production quicker.

As developers, it is essential to understand design patterns and how to use them to boost the development productivity.

Therefore, this article will discuss the Gang-of-Four Design Patterns and how to implement them using React.

Design Patterns: Gang-of-Four

As mentioned, designs Patterns help provide a language-independent, reusable, and scalable solution for typical design problems in software development. The Gang-of-Four refers to four authors who wrote the book — “Design Patterns: Elements of Reusable Object-Oriented Software” which fundamentally proposes 23 design patterns based on the principles of Object-Oriented Programming.

The four authors categorized design patterns into three groups.

  1. Creational Patterns: These patterns provide ways to instantiate an object (creating a component) while hiding the creation logic.
  2. Structural Patterns: These patterns help effectively define relationships between classes (components).
  3. Behavioral Patterns: These patterns are concerned with communication between components.

As all these patterns are language-independent. Developers can adapt these patterns to frameworks like React to design scalable and readable React components.

Implementing Design Patterns with React

We now have a brief understanding of design patterns and why developers should use them. Therefore, let’s see how we can use those design patterns in a React application.

For this demonstration, I recommend using React with TypeScript as it offers a statically typed language with interfaces, inheritance, and type support, which helps implement the patterns quickly.

Pre-requisites

Before proceeding, make sure that you have installed Node.js. You can run node -v to confirm the installation. If you've successfully installed Node.js, you should see the output below.

0*-4UWpVEAbIEBJE30.png

Figure 01 — Confirming Node.js installation

Afterward, run the command shown below to create a React TypeScript project named “react-design-patterns.”

npx create-react-app react-design-patterns --template typescript

Design Pattern 1: Singleton

What is a Singleton?

A Singleton is a creational design pattern that ensures that a class has only one instance with a single point of access.

When should you use Singleton?

For example, suppose a React application utilizes a global configuration object that holds the logged-in user information. Ideally, the application must re-use this information to obtain the current user’s information across the application.

This is a perfect scenario where developers can use the singleton pattern.

Implementing Singleton with React

We must address two areas when implementing the singleton pattern.

  1. The object must have a single instance
  2. It must only be accessible with a single point.

With React, developers can use a custom management module to satisfy these two conditions. For example, consider the code shown below.

// user type
interface SingletonConfigValues {
name?: string;
id?: string;
email?: string;
token?: string;
}

// private variable accessible only within current module
let loggedInUserStore: SingletonConfigValues | undefined = undefined;
const userActions = {
// configure the single instance logged in user
initializeUser: (user: SingletonConfigValues) => {
loggedInUserStore = user;
},
// retrieve the single instance of the logged in user
getUserInformation: () => {
return loggedInUserStore;
}
};

// export the methods so that components can use the single instance
export default userActions;

The snippet above shows a TypeScript module that manages the state of a single instance private variable — loggedInUserStore using two methods: getUserInformation - to retrieve the single instance and initializeUser - to configure the single instance.

This module provides a singleton approach to managing global user information. To use this module in a component, import the actions and invoke the methods as shown below.

import { useEffect, useState } from "react";
// obtain the single instance - directly user var is not accessible (private)
import userRetriever from '../../store/custom-singleton';

export const ComponentA = () => {
const [userInformation, setUserInformation] = useState<any>(undefined);
useEffect(() => {
setUserInformation(userRetriever.getUserInformation());
}, [userInformation]);
return (
<div>
<h1>Component A</h1><p>
{userInformation && (
<>
<span>Name: {userInformation.name}</span><br />
<span>Id: {userInformation.id}</span><br />
<span>Email: {userInformation.email}</span>
<br />
<span>Token: {userInformation.token}</span>
</>
}
</p>
/div>
;
};

The snippet above shows a component that retrieves a single instance.

import { useEffect } from "react";
import { ComponentA } from "./component-a";
// retrieve the single instance - directly user var is not acessible (private)
import userInformation from '../../store/custom-singleton';

export const Singleton = () => {

useEffect(() => {
// initialize the single instance variable
userInformation.initializeUser({
name: 'John Doe',
id: '123',
email: '[email protected]',
token: '123456789'
})
}, []);

return (
<div className="App"><div><ComponentA /></div></div>
);
};

The snippet above shows the single-user instance being configured inside the useEffect.

0*ffthEZeXatMzWNZy.png

Figure 02: Expected outcome via the Singleton Pattern

Pros of Singleton pattern

  • Using singleton makes the code scalable and maintainable
  • It avoids possible code duplication across multiple components.

Cons of Singleton pattern

  • The main drawback of using singletons is that it makes your code less testable as the global state of singletons gets preserved between unit tests.

Design Pattern 2: Observer

What is the Observer?

The observer is a behavioral pattern that helps define a subscription mechanism to notify the observers (components) regarding any change to the subject under observation.

When should you use Observer?

The most suitable use case to use observer in a React application is when the application depends on browser events. For example, suppose a React application has a component that needs to toggle its functionality based on internet availability. The component could subscribe to the browser’s internet (subject) event, get notified of changes, and then update its state accordingly.

Implementing Observer in React

When implementing observer:

  1. The component has to first subscribe to the event.
  2. Then, when the component unmounts, make sure to unsubscribe from the event.

Consider the code shown below.

import { useEffect, useState } from "react";

export const InternetAvailabilityObserver = () => {
const [isOnline, setOnline] = useState<any>(navigator.onLine);

useEffect(() => {
// subscribe to two events -> online and offline

// when online -> set online to true

// when offline -> set online to false

window.addEventListener("online", () => setOnline(true));  
window.addEventListener("offline", () => setOnline(false));
return () => {
// when component gets unmounted, remove the event listeners to prevent memory leaks

window.removeEventListener("online", () => setOnline(true)); 
window.removeEventListener("offline", () => setOnline(false));
};
}, []);

return (
<><h1>Internet Availability Observer</h1><p>
{isOnline ? (
<><span>
You are <b>online</b></span></>
) : (
<><span>
You are <b>offline</b></span></>
)}
</p>
</>
);
};

The snippet shown above shows the implementation of the observer in React. First, it creates two subscriptions with two subjects (online, and offline events). And when the subjects notify any changes, the callback functions get executed to update the component state.

0*5VNqDEWxSnauJE37.gif

Figure 03: Observer implementation with React

Pros of Observer pattern

  • It decouples the code between subject and observer, thus allowing more excellent maintainability.
  • Multiple observers can subscribe and unsubscribe from the subject at any given time.

Cons of Observer pattern

  • If the subscriptions do not get unsubscribed, React may keep listening for changes, cause memory leaks, and experience performance loss.

Design Pattern 3: Facade

What is the Facade pattern?

A Facade is a structural pattern that aims to provide a simplified way to interact with multiple components via a single API. Furthermore, it hides the complexity of the internal operations, thus making the code more readable. To implement this pattern, we require interfaces and classes. But, we can utilize the fundamental principle of this pattern and apply it in React.

When should you use Facade?

You may choose to implement the facade pattern in React applications when the application becomes complex. For example, a good use case of incorporating the facade pattern in React is when managing component state.

For instance, you may have a component that manages application users (add, remove, fetch, fetch one). Generally, this would result in many state variables and HTTP calls that clutter the code, which decreases its readability. Consider the snippet below.

export const NoFacade: FC = () => {
const [users, setUsers] = useState<any>([]);

const fetchUsers = useCallback(async () => {
const resp = await axios.get(`/api/users`);
setUsers(resp.data);
}, []);

useEffect(() => {
fetchUsers();
}, [fetchUsers]);

const handleUserDelete = async (id: string) => {
await axios.delete(`/api/users/${id}`);
setUsers(users.filter((user: any) => user.id !== id));
};

const handleCreateUser = async (user: any) => {
if (!users.find((u: any) => u.id === user.id)) {
await axios.post(`/api/users`, user);
setUsers([...users, user]);
} else {
console.log("User already exists");
}
};
return (
<><UserTable users={users} onDelete={handleUserDelete} /><UserCreateModal onCreate={handleCreateUser} /></>
);
};

At first glance, the snippet is not readable as it consists of complex HTTP requests and state variables. However, developers can group the complex HTTP requests and state variables and expose a single API to manipulate the data. This is when developers can use the facade pattern.

In React, developers can accomplish the same by grouping the complex code in a custom Hook and exposing the Hook methods for clients to consume.

Implementing Facade in React

Let us improve the snippet shown earlier by replacing the complex code with a custom Hook. The custom Hook will contain methods to create, fetch and delete users. The components can use the methods to consume the functionality without viewing the complex code that gets executed sequentially.

The code for the custom snippet is shown below.

import axios from "axios";
import { useState } from "react";

export const useFacadeUserAPI = () => {
const [users, setUsers] = useState<any>([]);
const [actionExecuting, setActionExecuting] = useState<boolean>(false);

// expose one method to get users

async function getUsers() {setActionExecuting(true);
try {
const resp = await axios.get("/api/users");
setUsers(resp.data);
} catch (err) {
console.log(err);
} finally {
setActionExecuting(false);
}
}

// expose method to create user

async function createUser(user: any) {setActionExecuting(true);
try {
await axios.post("/api/users", user);
setUsers([...users, user]);
} catch (err) {
console.log(err);
} finally {
setActionExecuting(false);
}
}

// expose method to delete a user

async function deleteUser(id: string) {setActionExecuting(true);
try {
await axios.delete(`/api/users/${id}`);
setUsers(users.filter((user: any) => user.id !== id));
} catch (err) {
console.log(err);
} finally {
setActionExecuting(false);
}
}

// return the methods that encapsulate the complex code// resulting in a cleaner client code

return {// the users

users,
// boolean to indicate if action occurs

actionExecuting,
// method to mutate the users via HTTP requests

getUsers,
createUser,
deleteUser,
};
};

The snippet above highlights the custom Hook that manages the user management. It returns a set of methods that components can use to interact with the API.

The updated code for the component is displayed below.

export const Facade: FC = () => {  
const userFacade = useFacadeUserAPI();
const { createUser, deleteUser, getUsers, users } = userFacade;

const fetchUsers = useCallback(async () => {
// replace with facade API method to simplify code
await getUsers();
}, [getUsers]);

useEffect(() => {
fetchUsers();
}, [fetchUsers]);

const handleUserDelete = async (id: string) => {
// replace with Facade method to hide complex code required in deleting
await deleteUser(id);
};

const handleCreateUser = async (user: any) => {
// replace with a facade method to hide complex code required in creating
await createUser(user);
};
return (
<><UserTable users={users} onDelete={handleUserDelete} /><UserCreateModal onCreate={handleCreateUser} /></>
);

};

The code snippet above highlights the updated code for the component. As you have observed, the custom Hook creates a more readable and less complex code and completely hides all complex business logic from the component.

Pros of Facade pattern

  • It makes the code reusable, which helps avoid code duplication.
  • It decouples the component’s business logic, allowing developers to write unit tests for the functions.

Cons of Facade pattern

  • It is difficult to refactor existing code and encapsulate it within a custom Hook. But, the pattern creates highly reusable and scalable code when implemented correctly.

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK