Pattern de Composição no ReactJS

Pattern de Composição no ReactJS

Um dos maiores desafios em se manter uma aplicação de larga escala feita com ReactJS, é a questão da organização da estrutura do nosso app.

Dependendo do nível de organização que os desenvolvedores aplicaram sobre a estrutura do projeto, será crucial para suas futuras manutenções, seja por parte da mesma equipe, ou de futuras equipes que poderão trabalhar no projeto. 

Tem uma grande máxima que diz assim "Leave it better than you found it.", que na tradução é deixe melhor do que você encontrou.

A ideia principal em torno desta frase, é usada para incentivar as pessoas a deixarem um lugar, ou uma situação em melhores condições do que estavam quando as encontraram, seja física, emocional ou socialmente.

E vai muito de encontro ao tópico desta lição, pois se você se deprar com um código mal estrutura e desorganizado? Por que não melhorá-lo?

Hoje nós vamos aprender a fazer o uso de um dos padrões de design de software, que visa criar e manter estruturas complexas a partir de módulos mais simples (componentes menores).

O que é o Composition Pattern?

Também conhecido como pattern de composição, ou "Composition Pattern", ele é uma abordagem presente na arquitetura de software que visa separar grande parte da lógica da sua aplicação em componentes cada vez menores.

O que ajuda a obter mais flexibilidade, manutenalibilidade e principalmente a reutilização de código. Fazendo com que diferentes partes do sistema possam ser compostas de forma independente, e combinadas para formar estruturas mais complexas e funcionais.

Gosto de pensar que essa composição é muito similar às peças de um quebra cabeça (mosáico), que quando juntas fazem parte de algo maior e formam uma só imagem.

Mas se e você olhar bem, cada uma das peças ali presentes, representam fotos de lugares ou pessoas, que quando juntas de forma organizada forma uma imagem maior, que é a foto de uma marca (BMW).

A diferença é que cada uma dessas peças funcionam de forma independente, de modo que não dependem de uma das outras, mas quando juntas, você já sabe o que acontece 😆

Criando seu projeto de testes

Antes de começarmos, vamos criar um novo projeto em ReactJS dedicado a esta lição. No meu caso criei um novo projeto chamado de composition-pattern:  

npx create-react-app composition-pattern

Após isso, não se esqueça de fazer aquela limpeza do código, e também aquela organizada nas pastas do src (organização por funcionalidade) 😁

Conceitos do padrão de composição

Atualmente o padrão de composição é envolto por 2 grandes conceitos e principios.

Composição sobre Herança

Composição sobre Herança: é um princípio de design orientada a objetos, que sugere a criação de classes bases e derivadas, onde visa criar uma relação rígida (de herança) entre as classes pai e filho.

Um dos maiores problemas relacionados a esse princípio é que seus componentes se tornam fortemente acoplados uns aos outros, o que dificulta a manutanção e a extenção do código.

Em ReactJS, não há herança de componentes no sentido tradicional da orientação a objetos. Sendo assim, pelo menos desse mal a gente não sofre 😂

Objetos Compostos

Objetos Compostos: é um conceito na qual nós temos estruturas de dados que contém outros objetos ou componentes como parte da sua composição.

Aqui, cada um dos objetos é especializado em uma determinada tarefa, o que promove a reutilização e flexibilização de código.

Em ReactJS, podemos aplicar o conceito de objetos compostos para criar componentes reutilizáveis e flexíveis, onde cada componente é especializado em uma determinada tarefa e pode ser combinado para formar estruturas de UI mais complexas. 

Que foi o que vimos durante a nossa jornada do ReactJS, onde tínhamos um componente de página (pasta pages) que poderia conter diversos outros tipos de componentes que eram separados dentro da pasta components.

Vamos ver um exemplo:

Components > Header > index.jsx:

import React from 'react';

const Header = ({ title }) => {
 return (
 <header>
 <h1>{title}</h1>
 </header>
 );
};

export default Header;

Components > ItemList > index.jsx:

import React from 'react';

const ItemList = ({ items }) => {
 return (
 <ul>
 {items.map((item, index) => (
 <Item key={index} item={item} />
 ))}
 </ul>
 );
};

const Item = ({ item }) => {
 return <li>{item}</li>;
};

export default ItemList;

App.js:

import React from 'react';
import Header from './Header';
import ItemList from './ItemList';

const App = () => {
 const items = ['Item 1', 'Item 2', 'Item 3'];

 return (
 <div className="app">
 <Header title="Lista de Itens" />
 <ItemList items={items} />
 </div>
 );
};

export default App;

Note que os componentes acima fazem o uso do princípio dos objetos compostos, desempenhando funções específicas e que podem ser aplicados de forma simples em nossas aplicações em ReactJS.

Além disso, alterações que são feitas em um componente NÃO DEVEM afetar diretamente outros componentes (guarde esse princípio, pois ele é conhecido como princípio da responsabilidade única).

Composition Pattern e sua relação com o ReactJS

Pelo teor que este tópico está tomando, é certo dizer que o composition pattern é uma excelente escolha no quesito de criação de UIs em nossa aplicações feitas com ReactJS.

Vamos entender alguns de seus benefícios:

Reutilização de Componentes) Componentes pequenos e especializados se unem para formar componentes maiores e mais complexos. O que promove a reutilização de código em várias partes da sua aplicação.

Separação de Responsabilidade) Cada componente é responsável por executar uma única tarefa específica em toda a sua aplicação. O que facilita a manutenção do código e o entendimento do mesmo.

Flexibilidade e Composição de forma Hierárquica) Seus componentes podem ser organizados de forma hierárquica, que quando combinados podem formar interfaces mais complexas.

Reduz código duplicado) Em vez de criar diversos botões que seguem o mesmo estilo em toda a sua aplicação, você cria apenas um único botão que é reutilizado em outros componentes ou em outras páginas.

Performance Otimizada) Esse padrão ajuda a manter a performance das nossas aplicações evitando renderizações desnecessárias.

Adeus "Prop Drilling") Graças a este padrão, você evita a necessidade de passar propriedades de forma desnecessária entre seus componentes, o que mantem seu código mais organizado, limpo e intuitívo.

Utilizando o Composition Pattern na Prática

Antes de mais nada, fica tranquilo, que tudo o que iremos aprender agora envolve somente organizações estruturais em nossas aplicações.

Não veremos nenhum hook novo, nenhuma funcionalidade nova... somente uma alternativa de reestruturação da forma como declaramos nossos componentes 🙂

Mas antes.... que tal montar essa telinha aqui dentro do nosso projeto de testes da forma como vinhamos fazendo? (Sem o Composition Pattern)

Para isso vamos criar um novo componente chamado de ProductCard dentro da pasta components.

ProductCard > index.jsx:

import React from 'react';
import './product-card.css';

const ProductCard = ({ tag, imageSrc, title, status, price, inStock }) => {
 return (
 <div className="product-card">
 {tag && <span className="card-tag">{tag}</span>}
 <div className="card-header">
 <img src={imageSrc} alt={title} />
 </div>
 <div className="card-body">
 <h4 className="product-title">{title}</h4>
 <p className="product-status">{inStock ? 'Disponível' : 'Fora de Estoque'}</p>
 <h3 className="product-prices">R$ {price.toFixed(2)}</h3>
 </div>
 <div className="card-footer">
 <button className="btn btn-secondary">
 <i className="bi bi-eye"></i>
 Visualizar
 </button>
 {inStock && (
 <button className="btn btn-primary">
 <i className="bi bi-bag"></i>
 Comprar
 </button>
 )}
 </div>
 </div>
 );
};


export default ProductCard;

ProductCard > product-card.css:

*{
	margin: 0;
	padding: 0;
	box-sizing: border-box;
}
body{
	background: rgb(80,253,35);
 background: linear-gradient(90deg, rgba(80,253,35,1) 0%, rgba(63,255,246,1) 35%, rgba(255,0,0,1) 93%, rgba(250,177,6,1) 100%);
	
}

#root{
 font-family: sans-serif;
	min-height: 100vh;
	display: flex;
	justify-content: center;
	align-items: center;
	/*By Bytewebster*/
}

.product-card{
	margin:20px;
	background-color: #fff;
	padding: 18px;
	border-radius: 10px;
	box-shadow: 0 0 15px rgba(0, 0, 0, 0.25);
}

.card-tag{
	background-color: rgba(179 , 75 , 248 , 0.3);
	padding: 5px 10px;
	color: #bf34bf;
	border-radius: 2px;
}
.card-header{
	padding: 30px 50px;
	text-align: center;
}
.card-header img{
	max-height: 200px;
}
.product-title{
	font-size: 24px;
	color: #353535;
	margin: 20px 0;
}

.product-status{
	color: #808080;
}
.product-price{
	font-size: 30px;
	color: #353535;
	margin: 20px 0;
}
.card-footer{
	margin-top:15px;
	display: flex;
	justify-content: space-between;
}

.btn{
	padding: 10px 40px;
	border: none;
	font-family: inherit;
	border-radius: 5px;
	font-size: 16px;
	cursor: pointer;
	width: 100%;
}

.btn-primary{
	background-color: #b34bf8;
	color: #fff;
}
.btn-primary:hover{
	background-color: #943EF0;
}

.btn-secondary{
	color: #808080;
}
.btn-secondary:hover{
	background-color: #9B9090;
	color: white;
}


.card-footer .btn:first-child{
	margin-right: 15px;
}

Não se esqueça de chamar o componente dentro do App.js:

import FoneJBL from './assets/fone-jbl.png';
import TecladoGamer from './assets/teclado-gamer.webp';
import ProductCard from './components/ProductCard';

const App = () => {
 return (
 <>
 <ProductCard
 tag="Novo"
 imageSrc={TecladoGamer}
 title="Teclado Gamer (Redragon)"
 status="Disponível"
 price={200.00}
 inStock={true}
 />
 <ProductCard
 imageSrc={FoneJBL}
 title="Fone JBL (Bluetooth)"
 status="Fora de Estoque"
 price={127.00}
 inStock={false}
 />
 </>
 );
};

export default App;

Lembrando que eu também fiz modificações diretas no index.html da pasta public, para suportar a importação dos ícones do Bootstrap de maneira mais fácil:

<!DOCTYPE html>
<html lang="en">
 <head>
 <meta charset="utf-8" />
 <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
 <meta name="viewport" content="width=device-width, initial-scale=1" />
 <meta name="theme-color" content="#000000" />
 <meta
 name="description"
 content="Web site created using create-react-app"
 />
 <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
 <!--
 manifest.json provides metadata used when your web app is installed on a
 user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
 -->
 <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
 <!--
 Notice the use of %PUBLIC_URL% in the tags above.
 It will be replaced with the URL of the `public` folder during the build.
 Only files inside the `public` folder can be referenced from the HTML.

 Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
 work correctly both with client-side routing and a non-root public URL.
 Learn how to configure a non-root public URL by running `npm run build`.
 -->
 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.2/font/bootstrap-icons.css">
 <title>React App</title>
 </head>
 <body>
 <noscript>You need to enable JavaScript to run this app.</noscript>
 <div id="root"></div>
 <!--
 This HTML file is a template.
 If you open it directly in the browser, you will see an empty page.

 You can add webfonts, meta tags, or analytics to this file.
 The build step will place the bundled scripts into the <body> tag.

 To begin the development, run `npm start` or `yarn start`.
 To create a production bundle, use `npm run build` or `yarn build`.
 -->
 </body>
</html>

O resultado final é o mesmo que vimos na imagem anterior:

Legal, como podemos perceber, nós criamos um único componente que faz o uso de diversos elementos HTML de forma aglomerada:

import React from 'react';
import './product-card.css';

const ProductCard = ({ tag, imageSrc, title, status, price, inStock }) => {
 return (
 <div className="product-card">
 {tag && <span className="card-tag">{tag}</span>}
 <div className="card-header">
 <img src={imageSrc} alt={title} />
 </div>
 <div className="card-body">
 <h4 className="product-title">{title}</h4>
 <p className="product-status">{inStock ? 'Disponível' : 'Fora de Estoque'}</p>
 <h3 className="product-prices">R$ {price.toFixed(2)}</h3>
 </div>
 <div className="card-footer">
 <button className="btn btn-secondary">
 <i className="bi bi-eye"></i>
 Visualizar
 </button>
 {inStock && (
 <button className="btn btn-primary">
 <i className="bi bi-bag"></i>
 Comprar
 </button>
 )}
 </div>
 </div>
 );
};


export default ProductCard;

O ruim disso, é que tudo fica muito junto e misturado, o que pode tornar difícil sua manutenção e a adição de novos elementos.

Foi pensando nisso que o Composition Pattern surgiu, pois como ele podemos separar cada elemento HTML em pequenos componentes, maaaas... para realizar essa modularização (refatoração), primeiro precisamos fazer um RAIO-X de modo a identificar o que pode ser separado ou não.

Pois bem, para recriar essa lógica, vamos dividir o componente ProductCard em componentes menores e reutilizáveis, da seguinte forma:

  • ProductTag para a tag do produto (se existir).
  • ProductImage para a imagem do produto.
  • ProductInfo para o título, status e preço do produto.
  • ProductButtons para os botões de ação (Visualizar e Comprar).

E você pode fazer isso criando esses componentes da seguinte forma:

ProductTag > index.jsx:

import React from 'react';

const ProductTag = ({ tag }) => {
 return (
 tag && <span className="card-tag">{tag}</span>
 );
};

export default ProductTag;

ProductImage > index.jsx:

import React from 'react';

const ProductImage = ({ src, alt }) => {
 return (
 <div className="card-header">
 <img src={src} alt={alt} />
 </div>
 );
};

export default ProductImage;

ProductInfo > index.jsx:

import React from 'react';

const ProductInfo = ({ title, status, price, inStock }) => {
 return (
 <div className="card-body">
 <h4 className="product-title">{title}</h4>
 <p className="product-status">{inStock ? 'Disponível' : 'Fora de Estoque'}</p>
 <h3 className="product-prices">R$ {price.toFixed(2)}</h3>
 </div>
 );
};

export default ProductInfo;

Com relação ao ProductInfo, você pode separá-lo em mais componentes, como ProductTitle, ProductStatus e ProductPrice, reflita!

ProductButtons > index.jsx:

import React from 'react';

const ProductButtons = ({ inStock }) => {
 return (
 <div className="card-footer">
 <button className="btn btn-secondary">
 <i className="bi bi-eye"></i>
 Visualizar
 </button>
 {inStock && (
 <button className="btn btn-primary">
 <i className="bi bi-bag"></i>
 Comprar
 </button>
 )}
 </div>
 );
};

export default ProductButtons;

Em seguida precisamos atualizar o nosso componente ProductCard de modo a importar todos esses sub-componentes que nós criamos:

ProductCard > index.jsx:

import React from 'react';
import ProductTag from './ProductTag';
import ProductImage from './ProductImage';
import ProductInfo from './ProductInfo';
import ProductButtons from './ProductButtons';

const ProductCard = ({ tag, imageSrc, title, status, price, inStock }) => {
 return (
 <div className="product-card">
 <ProductTag tag={tag} />
 <ProductImage src={imageSrc} alt={title} />
 <ProductInfo title={title} status={status} price={price} inStock={inStock} />
 <ProductButtons inStock={inStock} />
 </div>
 );
};

export default ProductCard;

Por fim, garanta que o App.js esteja setado dessa forma:

import FoneJBL from './assets/fone-jbl.png';
import TecladoGamer from './assets/teclado-gamer.webp';
import ProductCard from './components/ProductCard';

const App = () => {
 return (
 <>
 <ProductCard
 tag="Novo"
 imageSrc={TecladoGamer}
 title="Teclado Gamer (Redragon)"
 status="Disponível"
 price={200.00}
 inStock={true}
 />
 <ProductCard
 imageSrc={FoneJBL}
 title="Fone JBL (Bluetooth)"
 status="Fora de Estoque"
 price={127.00}
 inStock={false}
 />
 </>
 );
};

export default App;

E aqui estamos, com o mesmo resultado final:

A diferença é que a nossa aplicação está beeeem mais modular 😉

Isso é o Pattern Composition em ação!

Beleza... mas supondo que o ProductButtons execute uma determinada função externa, como fariamos?

Simples, você poderia receber algum tipo de identificador (ID) dentro do componente, e assim que ele for clicado, executar uma função existente dentro de um service 🙂

Arquivos da lição

Os arquivos dessa lição podem ser encontrados neste repositório do GitHub.

Conclusão

E então, o que acha que aplicar este tipo de padrão de design em seus projetos em ReactJS? 🙃

Até a próxima aula!