- 1. JavaScript design patterns #1. Singleton and the Module
- 2. JavaScript design patterns #2. Factories and their implementation in TypeScript
- 3. JavaScript design patterns #3. The Facade pattern and applying it to React Hooks
- 4. JavaScript design patterns #4. Decorators and their implementation in TypeScript
- 5. JavaScript design patterns #5. The Observer pattern with TypeScript
The essential thing when approaching design patterns is to utilize them in a way that improves our codebase. The above means that we sometimes can bend them to fit our needs. The Facade design pattern is commonly associated with object-oriented programming. That doesn’t change the fact that we can take its fundamentals and apply them to something else. In this article, we explore the Facade pattern both in a conventional setting and with React hooks.
The fundamentals of the facade pattern
The facade pattern aims to provide a simplified way to interact with multiple components by creating a single API. By masking the underlying interactions, it helps us to keep our code more readable. The facade pattern can also help us to group generic functionalities into a more specific context. It is especially useful in situations when our system is quite complex, and we can see some patterns in the way we interact with it.
Let’s say we have a set of classes and methods that we want to use.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
class Bed { makeTheBed() { console.log('The bed is ready'); } } class AirFreshener { spray() { console.log('A nice smell spreads through the air') } } class TrashCan { takeOutTrash() { console.log('The trash is taken out') } } class Dishwasher { fill() { console.log('The dishwasher is filled'); } wash() { console.log('The dishwasher is working'); return new Promise((resolve) => { resolve(); }); } empty() { console.log('The dishwasher is empty'); } } |
We often might find ourselves wanting to execute the above methods in a particular order. Doing so multiple times across our codebase would violate the Don’t Repeat Yourself (DRY) principle. To avoid the above, we can create a Facade class. It can keep track of the dependencies and execute our methods in a particular order.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class HouseCleaningFacade { constructor(bed, trashCan, airFreshener, dishwasher) { this.bed = bed; this.trashCan = trashCan; this.airFreshener = airFreshener; this.dishwasher = dishwasher; } cleanTheHouse() { this.bed.makeTheBed(); this.trashCan.takeOutTrash(); this.airFreshener.spray(); this.dishwasher.fill(); this.dishwasher.wash() .then(this.dishwasher.empty) } } |
Now we have a simple way to use a more complex set of actions with a lot of different parts. We can use it in a straightforward way and keep our application more readable.
1 2 3 4 5 6 7 8 |
const houseCleaning = new HouseCleaningFacade( new Bed(), new TrashCan(), new AirFreshener(), new Dishwasher() ); houseCleaning.cleanTheHouse(); |
It is limiting in comparison to executing every method separately, but it provides consistency across our application. If at some point we decide to change the logic in our Facade, it affects every part of the application in which we’ve used it.
Using the Facade pattern with third-party functionalities
It is also a common approach to use a Facade to interact with a third-party library or some complex native functionalities. A good example of the above is the Fetch API. It can be considered to be low-level compared to libraries like axios. Let’s create a simplified interface that deals with some of its shortages. We want to:
- automatically inject authentication tokens
- parse the body of the response so that we don’t need to call response.json() every time
- throw an error if the request did not succeed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
class API { constructor(authToken) { this.authToken = authToken; } constructHeaders() { const headers = new Headers(); headers.set('Authorization', this.authToken); return headers; } handleResponse(response) { if (response.ok) { return response.json(); } else { return Promise.reject({ status: response.status, statusText: response.statusText }); } } get(url, options) { return fetch(url, { headers: this.constructHeaders(), ...options, }) .then(this.handleResponse); } post(url, options) { return fetch(url, { method: 'POST', headers: this.constructHeaders(), ...options, }) .then(this.handleResponse); } put(url, options) { return fetch(url, { method: 'PUT', headers: this.constructHeaders(), ...options, }) .then(this.handleResponse); } delete(url, options) { return fetch(url, { method: 'DELETE', headers: this.constructHeaders(), ...options, }) .then(this.handleResponse); } } |
Now we have a class that wraps existing native functionalities with additional logic. You can easily expand it if you ever need some additional functionalities or if you need to adjust it for the API you use.
1 2 3 4 5 6 7 8 9 |
const api = new API('my-auth-token'); api.get('https://jsonplaceholder.typicode.com/users/1') .then(data => { console.log('User data', data); }) .catch(error => { console.error(error); }); |
If you want to know more about the Fetch API, check out Comparing working with JSON using the XHR and the Fetch API
Applying the Facade principles to React Hooks
Even though the Facade pattern is usually referred to when using classes, we can use its principles when designing our custom React Hooks. First, let’s look at an example that might need some refactoring. Then, we are going to attempt to fix it by applying the Facade pattern.
Let’s look into an example that needs some refactoring:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
import React, { useState } from 'react'; import AddUserModal from './AddUserModal'; import UsersTable from './UsersTable'; const Users = () => { const [users, setUsers] = useState([]); const [isAddUserModalOpened, setAddUserModalVisibility] = useState(false); function openAddUserModal() { setAddUserModalVisibility(true); } function closeAddUserModal() { setAddUserModalVisibility(false); } function addUser(user) { setUsers([ ...users, user ]) } function deleteUser(userId) { const userIndex = users.findIndex(user => user.id === userId); if (userIndex > -1) { const newUsers = [...users]; newUsers.splice(userIndex, 1); setUsers( newUsers ); } } return ( <> <button onClick={openAddUserModal}>Add user</button> <UsersTable users={users} onDelete={deleteUser} /> <AddUserModal isOpened={isAddUserModalOpened} onClose={closeAddUserModal} onAddUser={addUser} /> </> ) }; export default Users; |
The first thing we notice above is a lot of different hooks. It is not very readable and certainly not reusable. Even though the useState hook is very generic, we put it in a specific context. All of the hooks combined, give us the possibility of managing the table of the users. Due to all of the above, it is a perfect place to use some of the principles of the Facade design pattern.
We can easily group the above functionalities into custom hooks:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
function useUsersManagement() { const [users, setUsers] = useState([]); function addUser(user) { setUsers([ ...users, user ]) } function deleteUser(userId) { const userIndex = users.findIndex(user => user.id === userId); if (userIndex > -1) { const newUsers = [...users]; newUsers.splice(userIndex, 1); setUsers( newUsers ); } } return { users, addUser, deleteUser } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function useAddUserModalManagement() { const [isAddUserModalOpened, setAddUserModalVisibility] = useState(false); function openAddUserModal() { setAddUserModalVisibility(true); } function closeAddUserModal() { setAddUserModalVisibility(false); } return { isAddUserModalOpened, openAddUserModal, closeAddUserModal } } |
The only thing that’s left is to use our newly created Facade. By doing so, we make the component a lot lighter.
We also make it more testable by moving the logic outside of the component. Nowadays, packages like the react-testing-library have a set of tools for testing hooks.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
import React from 'react'; import AddUserModal from './AddUserModal'; import UsersTable from './UsersTable'; import useUsersManagement from "./useUsersManagement"; import useAddUserModalManagement from "./useAddUserModalManagement"; const Users = () => { const { users, addUser, deleteUser } = useUsersManagement(); const { isAddUserModalOpened, openAddUserModal, closeAddUserModal } = useAddUserModalManagement(); return ( <> <button onClick={openAddUserModal}>Add user</button> <UsersTable users={users} onDelete={deleteUser} /> <AddUserModal isOpened={isAddUserModalOpened} onClose={closeAddUserModal} onAddUser={addUser} /> </> ) }; export default Users; |
The above helps us in multiple ways. It makes our code highly reusable, following the DRY principle. We can also easily modify it. If we ever decide to implement some state management, such as Redux, we would just need to adjust our custom hooks. Putting the logic of a component behind a Facade also simplifies the code a lot and makes it more readable.
Summary
The Facade design pattern proves to be very useful. With it, we can make our code cleaner and more reusable. In this article, we’ve gone through the fundamentals of what the Facade is. We’ve also written some examples of how it might be useful. Aside from using it conventionally, we’ve also implemented the concept of Facade in React Hooks. By doing the above, we’ve proven that the Facade is, in general, a concept worth looking into if we want to improve the quality of our code.
Great post, help me a lot to understand how facade pattern can be used in frontend part of the app. Thanks 😉
Totally disagree. Your Facades are hard to understand and won’t clean code. They clean part of the code at the cost of extra files and making somewhere else dirty. They also increase the expertise level of the codebase.
Agree with this, I feel like this adds a lot of unnecessary abstraction to the project.
I think the first Example HouseCleaningFacade is more like template pattern.
Creating a class to provide a single function is how Go4 implemented this pattern … in Java — most likely because that language does not accept functions on the top level. As JavaScript / TypeScript doesn’t have the same constraint it just feels wrong to create a class for this purpose.