Problem 🤔
In modern frontend development, component libraries such as MUI, Angular Material, Chakra UI, etc. are usually used. These libraries help us be faster in our development, but they also have their drawbacks. For example:
- As the project grows and these components are used throughout our application, we become more and more coupled to these same libraries. If in the future we want to use another library for a component, there are many pages that would have to be touched to replace it.
- Time also passes and these libraries deprecate certain interfaces and update many others. Who hasn't experienced that when updating a version of a CSS framework they change the name of a CSS class and now it's time to change this name for the entire application.
- Furthermore, if you use several libraries in your project, every time you want to know what interface a component has you have to go to the official documentation of said library to see what properties it has and how they are used. Making your code difficult to understand and maintain. How can we avoid these problems and make our code more readable, modular, and reusable? In this article, I'll show you how you can create your own custom components using front-end development best practices.
Practical example
To illustrate the problem and the solution, let's use a simple example. Imagine that you have to create a page that lists desserts ordered by their calories. If you use MUI, a component library based on React, the first thing you will have to do is import the different components that make up the table:
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
Then you will use these components within the page where you want the table:
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>Dessert (100g serving)</TableCell>
<TableCell align="right">Calories</TableCell>
<TableCell align="right">Fat (g)</TableCell>
<TableCell align="right">Carbs (g)</TableCell>
<TableCell align="right">Protein (g)</TableCell>
</TableRow>
</TableHead>
<TableBody>
{rows.map((row) => (
<TableRow
key={row.name}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
>
<TableCell component="th" scope="row">
{row.name}
</TableCell>
<TableCell align="right">{row.calories}</TableCell>
<TableCell align="right">{row.fat}</TableCell>
<TableCell align="right">{row.carbs}</TableCell>
<TableCell align="right">{row.protein}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
As you can see, this table is very flexible and dynamic, but it also has some disadvantages:
- When using MUI components directly on the page, your code is coupled to this library and depends on its changes and updates.
- Also, if you use the same table component on different pages to display other things, your entire app is tied to this library, making it hard to maintain and scale.
Solution 🧐
In this part, I am going to explain how you can apply the facade pattern to improve the quality and maintainability of your code. The facade pattern is a type of structural design pattern used to simplify interaction with a complex subsystem. It consists of creating a facade object that implements a simple interface and unifies the different interfaces of the subsystem or subsystems. This reduces the coupling between clients and the subsystem and makes it easier to use and understand.
Application example
To illustrate the use of the facade pattern, let's take the example of the table that lists desserts ordered by their calories. Instead of directly using the MUI components on the page that we're going to display to the end user, we're going to create a custom component that encapsulates the logic and styling of the table. This component will be our facade, and it will allow us to abstract the complexity of the MUI components and decouple our code from the library.
The code for our page would look something like this:
import TableComponent from '@lib/componets/data-display/table';
const headElements: HeadElement<Desert>[] = [
{ id: 'name', label: 'Postre' },
{ id: 'calories', label: 'Calorías' },
{ id: 'fat', label: 'Grasa' },
{ id: 'carbs', label: 'Carbohidratos' },
{ id: 'protein', label: 'Proteinas' }
];
const data = [{name: 'Yogur', calories: 200, fat: 100, carbs: 0, protein: 130}];
export default function Page() {
return (
<TableComponent<Desert>
rowKeys={['name', 'calories', 'fat', 'carbs', 'protein']}
rows={data}
headElements={headElements}
/>
)
}
As you can see, the code is much simpler and readable. We just have to import our custom component TableComponent
and pass it the data we want to display as props. We don't have to worry about the internal details of the MUI components, nor their possible changes or updates.
The code for our TableComponent
component would be something like this:
// TableComponent.tsx
import React from 'react';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
type HeadElement<T> = {
id: keyof T;
label: string;
};
type Props<T> = {
rowKeys: Array<keyof T>;
rows: Array<T>;
headElements: Array<HeadElement<T>>;
};
function TableComponent<T>(props: Props<T>) {
const { rowKeys, rows, headElements } = props;
return (
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label="simple table">
<TableHead>
<TableRow>
{headElements.map((headElement) => (
<TableCell key={headElement.id}>{headElement.label}</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{rows.map((row) => (
<TableRow
key={row.name}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
>
{rowKeys.map((rowKey) => (
<TableCell key={rowKey} align="right">
{row[rowKey]}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
}
export default TableComponent;
Here we can see how our TableComponent
component implements the table logic and styling using the MUI components and the props it receives. Our component is generic and can receive any type of data as a prop, as long as the keys and header elements are specified. This way, we can reuse our component in different pages and projects without depending on the MUI library.
Conclusions 🙂
Finally, let's summarize the advantages and disadvantages of the facade pattern:
- Advantages:
- Helps us decouple our project from third-party libraries.
- Makes it easier to maintain and update our code.
- It can be applied to other dependencies of our application.
- Disadvantages:
- Our facade can become an all-powerful object (we must respect the single responsibility principle).
- It can hide the complexity of the subsystem and make it difficult to debug.
I hope this article has helped you better understand the facade pattern and how you can use it to improve the quality and maintainability of your code. If you have any questions or comments, do not hesitate to contact me. 😊
Top comments (0)