Nota: apenas traduzi o texto abaixo e postei aqui. Atualizei um pouco apenas os códigos. As referências estão no fim deste artigo.
Publicado originalmente em https://www.developerway.com. O site tem mais artigos como este. 😉
Uma das coisas mais interessantes e desafiadoras no React é não dominar algumas técnicas avançadas de gerenciamento de state ou como usar o Context de maneira adequada. Mais complicado de acertar é como e quando devemos separar nosso código em components independentes e como compô-los adequadamente. Muitas vezes vejo desenvolvedores caindo em duas armadilhas: ou eles não extraem eles rápido o suficiente e acabam com enormes "monólitos" de components que fazem muitas coisas ao mesmo tempo e que são um pesadelo para manter. Ou, especialmente depois de terem sido queimados algumas vezes pelo pattern anterior, eles extraem components muito cedo, o que resulta em uma combinação complicada de múltiplas abstrações, código com engenharia excessiva e, novamente, um pesadelo para manter.
O que quero fazer hoje é oferecer algumas técnicas e regras que possam ajudar a identificar quando e como extrair components no prazo e como não cair na armadilha do excesso de engenharia. Mas primeiro, vamos atualizar alguns princípios básicos: o que é composition (composição) e quais composition patterns estão disponíveis para nós?
React components composition patterns
Components simples
Components simples são um bloco de construção básico do React. Eles podem aceitar props, ter algum state e podem ser bastante complicados, apesar do nome. Um Button component que aceita properties title
e onClick
e renderiza uma button tag é um component simples.
import { ComponentPropsWithoutRef, ReactNode } from 'react';
type ButtonProps = Omit<ComponentPropsWithoutRef<'button'>, 'title'> & {
title: ReactNode
};
const Button = ({ title, onClick }: ButtonProps) => (
<button onClick={onClick}>{title}</button>
);
Qualquer component pode renderizar outros components – isso é composition. Um Navigation
component que renderiza esse Button
- também um component simples, que compõe outros components:
// onClickHandler declarado aqui
const Navigation = () => (
<>
{/** Renderizando o Button component no Navigation component. **/}
{/** Composition! **/}
<Button title="Create" onClick={onClickHandler} />
{/** ... algum outro código de navigation **/}
</>
);
Com esses components e sua composition, podemos implementar UI tão complicada quanto quisermos. Tecnicamente, nem precisamos de outros patterns e técnicas, todos eles são apenas úteis que apenas melhoram a reutilização de código ou resolvem apenas casos de uso específicos.
Container components
Container components é uma técnica de composition mais avançada. A única diferença dos components simples é que eles, entre outras props, permitem passar a prop especial children, para os quais o React possui sua própria sintaxe. Se nosso Button do exemplo anterior aceitasse não o title, mas children, seria escrito assim:
// o código é exatamente o mesmo! basta substituir "title" por "children"
import { ComponentPropsWithoutRef } from 'react';
type ButtonProps = ComponentPropsWithoutRef<'button'>;
const Button = ({ children, onClick }: ButtonProps) => (
<button onClick={onClick}>{children}</button>
);
O que não é diferente do title na perspectiva do Button. A diferença está no lado do "consumer", a sintaxe children é especial e se parece com suas tags HTML normais:
// onClickHandler declarado aqui
const Navigation = () => {
return (
<>
<Button onClick={onClickHandler}>Create</Button>
{/** ... algum outro código de navigation **/}
</>
);
};
Qualquer coisa pode entrar em children
. Podemos, por exemplo, adicionar um Icon
component além do texto, e então Navigation
terá uma composition de components Button
e Icon
:
// onClickHandler declarado aqui
const Navigation = () => (
<>
<Button onClick={onClickHandler}>
{/** Icon component é renderizado dentro de button, **/}
{/** mas button não sabe **/}
<Icon />
<span>Create</span>
</Button>
{/** ... algum outro código de navigation **/}
</>
);
Navigation controla o que acontece com children; da perspectiva de Button, ela apenas renderiza tudo o que o "consumer" deseja.
Veremos mais exemplos práticos dessa técnica mais adiante neste artigo.
Existem outros composition patterns, como higher-order components, passando components como props ou context, mas esses devem ser usados apenas para casos de uso muito específicos. Components simples e container components são os dois principais pilares do desenvolvimento do React, e é melhor aperfeiçoar o uso deles antes de tentar introduzir técnicas mais avançadas.
Agora que você os conhece, está pronto para implementar a UI mais complicada que precisar!
Ok, estou brincando, não vou fazer um artigo do tipo "como desenhar uma coruja" aqui. 😅
É hora de algumas regras e diretrizes para que possamos realmente desenhar aquela coruja e construir React apps complicados com facilidade.
Quando é um bom momento para extrair components?
As principais regras de desenvolvimento e decomposition do React que gosto de seguir, e quanto mais codifico, mais fortemente me sinto a respeito delas, são:
- sempre comece a implementação do topo
- extrair components somente quando houver uma necessidade real
- sempre comece com components "simples", introduza outras técnicas de composition somente quando houver real necessidade delas
Qualquer tentativa de pensar "com antecedência" ou começar "de baixo para cima" a partir de pequenos components reutilizáveis sempre termina em APIs de components excessivamente complicadas ou em components que carecem de metade da funcionalidade necessária.
E a primeira regra para quando um component precisa ser decomposto em components menores é quando um component é muito grande. Um bom tamanho para um component para mim é quando ele cabe inteiramente na tela do meu laptop. Se eu precisar realizar scroll para ler o código do component, é um sinal claro de que ele é muito grande.
Vamos começar a codificar agora, para ver como isso funciona na prática. Vamos implementar uma página Jira típica do zero hoje, nada menos (bem, mais ou menos, pelo menos vamos começar 😅).
Esta é a tela de uma página de edição do meu projeto pessoal onde guardo minhas receitas favoritas encontradas online 🍣. Lá precisamos implementar, como você pode ver:
- top bar (barra superior) com logo, alguns menus, button "create" e search bar (barra de pesquisa)
- sidebar (barra lateral) à esquerda, com o nome do projeto, seções collapsable de "planejamento" e "desenvolvimento" com itens dentro (também divididos em grupos), com uma seção sem nome com menu items abaixo
- uma grande seção de "conteúdo da página", onde são mostradas todas as informações sobre o issue atual
Então, vamos começar a codificar tudo isso em apenas um grande component. Provavelmente será algo assim:
export const JiraIssuePage = () => (
<div className="app">
<div className="top-bar">
<div className="logo">logo</div>
<ul className="main-menu">
<li>
<a href="#">Your work</a>
</li>
<li>
<a href="#">Projects</a>
</li>
<li>
<a href="#">Filters</a>
</li>
<li>
<a href="#">Dashboards</a>
</li>
<li>
<a href="#">People</a>
</li>
<li>
<a href="#">Apps</a>
</li>
</ul>
<button className="create-button">Create</button>
{/** mais top bar items aqui, como **/}
{/** SearchBar e ProfileMenu **/}
</div>
<div className="main-content">
<div className="sidebar">
<div className="sidebar-header">ELS project</div>
<div className="sidebar-section">
<div
className="sidebar-section-title"
>Planning</div>
<button className="board-picker">ELS board</button>
<ul className="section-menu">
<li>
<a href="#">Roadmap</a>
</li>
<li>
<a href="#">Backlog</a>
</li>
<li>
<a href="#">Kanban board</a>
</li>
<li>
<a href="#">Reports</a>
</li>
<li>
<a href="#">Roadmap</a>
</li>
</ul>
<ul className="section-menu">
<li>
<a href="#">Issues</a>
</li>
<li>
<a href="#">Components</a>
</li>
</ul>
</div>
<div className="sidebar-section">
sidebar development section
</div>
{/** outras sections **/}
</div>
<div className="page-content">
{/** ... aqui haverá muito código **/
{/** para visualização da issue **/}
</div>
</div>
</div>
);
Agora, não implementei nem metade dos itens necessários lá, sem falar na lógica, e o component já é grande demais para ser lido de uma só vez. Veja em codesandbox. Isso é bom e esperado! Portanto, antes de prosseguir, é hora de dividi-lo em partes mais gerenciáveis.
A única coisa que preciso fazer é criar alguns novos components e copiar e colar o código neles. Não tenho nenhum caso de uso para nenhuma das técnicas avançadas (ainda), então tudo será um component simples.
Vou criar um Topbar
component, que terá tudo relacionado à top bar, um Sidebar
component, para tudo relacionado à sidebar, como você pode imaginar, e um Issue
component para a parte principal que não iremos abordar hoje. Dessa forma, nosso component principal JiraIssuePage
fica com este código:
export const JiraIssuePage = () => (
<div className="app">
<Topbar />
<div className="main-content">
<Sidebar />
<div className="page-content">
<Issue />
</div>
</div>
</div>
);
Agora vamos dar uma olhada na implementação do novo Topbar component:
export const Topbar = () => (
<div className="top-bar">
<div className="logo">logo</div>
<ul className="main-menu">
<li>
<a href="#">Your work</a>
</li>
<li>
<a href="#">Projects</a>
</li>
<li>
<a href="#">Filters</a>
</li>
<li>
<a href="#">Dashboards</a>
</li>
<li>
<a href="#">People</a>
</li>
<li>
<a href="#">Apps</a>
</li>
</ul>
<button className="create-button">Create</button>
{/** mais top bar items aqui, como **/}
{/** SearchBar e ProfileMenu **/}
</div>
);
Se eu implementasse todos os itens lá (searchbar, todos os submenus, icons à direita), esse component também seria muito grande, então também precisa ser dividido. E este é sem dúvida um caso mais interessante que o anterior. Porque, tecnicamente, posso simplesmente extrair o MainMenu
component dele para torná-lo pequeno o suficiente.
export const Topbar = () => (
<div className="top-bar">
<div className="logo">logo</div>
<MainMenu />
<button className="create-button">Create</button>
{/** mais top bar items aqui, como **/}
{/** SearchBar e ProfileMenu **/}
</div>
);
Mas extrair apenas MainMenu
tornou o Topbar
component um pouco mais difícil de ler para mim. Antes, quando eu olhava para Topbar
, eu poderia descrevê-lo como "um component que implementa várias coisas no topbar", e focar nos detalhes apenas quando preciso. Agora a descrição seria "um componente que implementa várias coisas na top bar E compõe algum component aleatório do MainMenu
". O fluxo de leitura está arruinado.
Isso me leva à minha segunda regra de decomposition de components: ao extrair components menores, não pare no meio do caminho. Um component deve ser descrito como um "component que implementa várias coisas" ou como um "component que compõe vários components juntos”, e não ambos.
Portanto, uma implementação muito melhor do Topbar component seria assim:
export const Topbar = () => (
<div className="top-bar">
<Logo />
<MainMenu />
<Create />
{/** mais top bar items aqui, como **/}
{/** SearchBar e ProfileMenu **/}
</div>
);
Muito mais fácil de ler agora!
E exatamente a mesma história com o Sidebar component - muito grande se eu tivesse implementado todos os itens, então preciso dividi-lo:
export const Sidebar = () => (
<div className="sidebar">
<Header />
<PlanningSection />
<DevelopmentSection />
{/** outras sidebar sections **/}
</div>
);
Veja o exemplo completo na caixa de códigos.
E então basta repetir essas etapas sempre que um componente ficar muito grande. Em teoria, podemos implementar toda esta página do Jira usando nada mais do que componentes simples.
Quando é a hora de apresentar os Container components?
Agora a parte divertida: vamos ver quando devemos apresentar algumas técnicas avançadas e por quê. Começando com Container components.
Primeiro, vamos dar uma olhada no design novamente. Mais especificamente - nas seções Planning e Development no sidebar menu.
Eles não apenas compartilham o mesmo design do title, mas também o mesmo comportamento: clicar no title recolhe a seção e, no modo "collapsed", o ícone de minisseta aparece. E nós o implementamos como dois components diferentes - PlanningSection
e DevelopmentSection
. Eu poderia, é claro, apenas implementar a lógica de "collapse" em ambos, afinal é apenas uma questão de state simples:
import { useState } from 'react';
const PlanningSection = () => {
const [isCollapsed, setIsCollapsed] = useState(false);
return (
<div className="sidebar-section">
<div
onClick={() => setIsCollapsed(!isCollapsed)}
className="sidebar-section-title"
>
Planning
</div>
{!isCollapsed && <>...todo o resto do código</>}
</div>
);
};
Mas:
- há muita repetição mesmo entre esses dois componentes
- o conteúdo dessas seções é realmente diferente para cada tipo de projeto ou tipo de página, portanto, ainda mais repetição no futuro próximo
Idealmente, quero encapsular a lógica do comportamento collapsed/expanded e o design do title, deixando diferentes seções com controle total sobre os itens que estão dentro. Este é um caso de uso perfeito para os Container components. Posso simplesmente extrair tudo do exemplo de código acima em um component e passar menu items como children
. Teremos um CollapsableSection
component:
import { ReactNode, useState } from 'react';
type CollapsableSectionProps = {
children: ReactNode;
title: ReactNode;
};
const CollapsableSection = ({
children,
title
}: CollapsableSectionProps) => {
const [isCollapsed, setIsCollapsed] = useState(false);
return (
<div className="sidebar-section">
<div
className="sidebar-section-title"
onClick={() => setIsCollapsed(!isCollapsed)}
>
{title}
</div>
{!isCollapsed && <>{children}</>}
</div>
);
};
e PlanningSection
(e DevelopmentSection
e todas as outras seções futuras) se tornarão apenas isto:
// import de CollapsableSection
const PlanningSection = () => (
<CollapsableSection title="Planning">
<button className="board-picker">ELS board</button>
<ul className="section-menu">... todos os menu items aqui</ul>
</CollapsableSection>
);
Uma história muito semelhante acontecerá com nosso root component JiraIssuePage
. No momento está assim:
// todos os imports dos components aqui
export const JiraIssuePage = () => (
<div className="app">
<Topbar />
<div className="main-content">
<Sidebar />
<div className="page-content">
<Issue />
</div>
</div>
</div>
);
Mas assim que começarmos a implementar outras páginas acessíveis a partir da sidebar, veremos que todas sigam exatamente o mesmo pattern - sidebar e topbar permanecem as mesmas, e apenas a área "conteúdo da página" muda. Graças ao trabalho de decomposition que fizemos antes, podemos simplesmente copiar e colar esse layout em cada página - afinal, não é tanto código. Mas como todos são exatamente iguais, seria bom apenas extrair o código que implementa todas as partes comuns e deixar apenas os components que mudam para as páginas específicas. Mais uma vez, um caso perfeito para o "container" component:
import { ReactNode } from 'react';
type JiraPageLayoutProps = {
children: ReactNode;
};
const JiraPageLayout = ({ children }: JiraPageLayoutProps) => (
<div className="app">
<Topbar />
<div className="main-content">
<Sidebar />
<div className="page-content">{children}</div>
</div>
</div>
);
E nosso JiraIssuePage
(e futuros JiraProjectPage
, JiraComponentsPage
, etc, todas as futuras páginas acessíveis na sidebar) se torna apenas isto:
export const JiraIssuePage = () => (
<JiraPageLayout>
<Issue />
</JiraPageLayout>
);
Se eu quisesse resumir a regra em apenas uma frase, poderia ser esta: extrair Container components quando houver necessidade de compartilhar alguma lógica visual ou comportamental que encapsule elements que ainda precisam estar sob controle do "consumer".
Container Components – caso de uso de desempenho
Outro caso de uso muito importante para Container components é melhorar o desempenho dos components. Tecnicamente a performance foge um pouco do assunto para a conversa sobre composition, mas seria um crime não mencioná-la aqui.
No Jira real, o Sidebar
component pode ser draggable - você pode resize ele fazendo o dragging dele para a esquerda e para a direita pela borda. Como implementaríamos algo assim? Provavelmente introduziríamos um Handle
component, algum state para a width
da sidebar e então faríamos o listen do "mousemove" event. Uma implementação rudimentar seria mais ou menos assim:
// import do Handle component
import { useState, useRef, useEffect } from 'react';
export const Sidebar = () => {
const [width, setWidth] = useState(240);
const [startMoving, setStartMoving] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!ref.current) return;
const changeWidth = (e: MouseEvent) => {
if (!startMoving) return;
if (!ref.current) return;
const left = ref.current.getBoundingClientRect().left;
const wi = e.clientX - left;
setWidth(wi);
};
ref.current.addEventListener('mousemove', changeWidth);
return () => ref.current?.removeEventListener('mousemove', changeWidth);
}, [startMoving, ref]);
const onStartMoving = () => {
setStartMoving(true);
};
const onEndMoving = () => {
setStartMoving(false);
};
return (
<div
className="sidebar"
ref={ref}
onMouseLeave={onEndMoving}
style={{ width: `${width}px` }}
>
<Handle onMouseDown={onStartMoving} onMouseUp={onEndMoving} />
{/* ... o resto do código */}
</div>
);
};
Há, no entanto, um problema aqui: cada vez que movemos o mouse, iremos disparar uma atualização de state, que por sua vez acionará a nova renderização de todo o Sidebar
component. Embora em nossa sidebar rudimentar não seja perceptível, isso pode tornar o "dragging" visivelmente lento quando o component se torna mais complicado. Container components são uma solução perfeita para isso: tudo o que precisamos é extrair todas as operações pesadas de state em um Container component e passar todo o resto pelos children
.
import { ReactNode } from 'react';
type DraggableSidebarProps = { children: ReactNode };
const DraggableSidebar = ({ children }: DraggableSidebarProps) => {
// todo o código de gestão do estado como antes
return (
<div
className="sidebar"
ref={ref}
onMouseLeave={onEndMoving}
style={{ width: `${width}px` }}
>
<Handle onMouseDown={onStartMoving} onMouseUp={onEndMoving} />
{/** children não seré afetado pelos **/}
{/** re-renders deste component **/}
{children}
</div>
);
};
E nosso Sidebar
component se transformará nisso:
// todos os imports dos components
export const Sidebar = () => (
<DraggableSidebar>
<Header />
<PlanningSection />
<DevelopmentSection />
{/* outras seções */}
</DraggableSidebar>
);
Dessa forma, o DraggableSidebar
component ainda será renderizado novamente a cada mudança de state, mas será muito barato, pois é apenas uma div. E tudo o que chega em children não será afetado pelas atualizações de state deste component.
Veja todos os exemplos de container components neste codesandbox. E para comparar o caso de uso de re-renders ruims, consulte este codesandbox. Preste atenção ao console output enquanto realiza o dragging do sidebar nesses exemplos - o PlanningSection
component loga constantemente na implementação "ruim" e apenas uma vez na implementação "boa".
E se você quiser saber mais sobre vários patterns e como eles influenciam o desempenho do React, você pode achar esses artigos interessantes: Como escrever código React de alto desempenho: regras, patterns, o que fazer e o que não fazer, Por que custom react hooks podem destruir o desempenho do seu app, Como escrever React apps de alto desempenho com Context
Este state pertence a este component?
Outra coisa, além do tamanho, que pode sinalizar que um component deve ser extraído, é o gerenciamento de state. Ou, para ser mais preciso, gerenciamento de state que é irrelevante para a funcionalidade do component. Deixe-me mostrar o que quero dizer.
Um dos itens da sidebar no Jira real é o item "Add shortcut", que abre um modal dialog quando você clica nele. Como você implementaria isso em nosso app? O modal dialog em si obviamente será seu próprio component, mas onde você introduziria o state que abre ele? Algo assim?
// import do ModalDialog component
import { useState } from 'react';
const SomeSection = () => {
const [showAddShortcuts, setShowAddShortcuts] = useState(false);
return (
<div className="sidebar-section">
<ul className="section-menu">
<li>
<span onClick={() => setShowAddShortcuts(true)}>
Add shortcuts
</span>
</li>
</ul>
{showAddShortcuts && (
<ModalDialog onClose={() => setShowAddShortcuts(false)} />
)}
</div>
);
};
Você pode ver algo assim em todos os lugares e não há nada de criminoso nesta implementação. Mas se eu estivesse implementando e quisesse tornar esse component perfeito do ponto de vista da composition, extrairia esse state e os components relacionados a ele externamente. E a razão é simples: esse state não tem nada a ver com o SomeSection
component. Este state controla um modal dialog que aparece quando você clica no shortcuts item (de atalhos). Isso torna a leitura deste component um pouco mais difícil para mim - vejo um component que é "section" e a próxima linha é algum state aleatório que não tem nada a ver com "section". Então, em vez da implementação acima, eu extrairia o item e o state que realmente pertence a esse item em seu próprio component:
// import do ModalDialog component
import { useState } from 'react';
const AddShortcutItem = () => {
const [showAddShortcuts, setShowAddShortcuts] = useState(false);
return (
<>
<span onClick={() => setShowAddShortcuts(true)}>Add shortcuts</span>
{showAddShortcuts && (
<ModalDialog onClose={() => setShowAddShortcuts(false)} />
)}
</>
);
};
E o section component se torna muito mais simples como bônus:
// import do AddShortcutItem component
const OtherSection = () => {
return (
<div className="sidebar-section">
<ul className="section-menu">
<li>
<AddShortcutItem />
</li>
</ul>
</div>
);
};
Veja ele no codesandbox.
Pela mesma lógica, no Topbar
component eu moveria o state futuro que controla os menus para um SomeDropdownMenu
component, todos os states relacionados à pesquisa para o Search
component e tudo relacionado à abertura do dialog "create issue" para o CreateIssue
component.
O que torna um component um "bom component"?
Uma última coisa antes de encerrar por hoje. No resumo quero escrever "o segredo de escrever apps escaláveis em React é extrair bons components no momento certo". Já cobrimos o "momento certo", mas o que exatamente é um "bom component"? Depois de tudo o que abordamos sobre composition até agora, acho que estou pronto para escrever uma definição e algumas regras aqui.
Um "bom component" é aquele que posso ler facilmente e entender o que faz à primeira vista.
Um "bom component" deve ter um bom nome autodescritivo. Sidebar
para um component que renderiza sidebar é um bom nome. CreateIssue
para um component que lida com a criação de issues é um bom nome. SidebarController
para um component que renderiza sidebar items específicos para a página "Issues" não é um bom nome (o nome indica que o component tem algum propósito genérico, não específico para uma página específica).
Um "bom component" não faz coisas que sejam irrelevantes para o seu propósito declarado. O Topbar
component que renderiza apenas itens na top bar e controla apenas o comportamento da topbar é um bom component. O Sidebar
component, que controla o state de várias modal dialogs, não é o melhor component.
Fechando bullet points
Agora posso escrever 😄! O segredo de escrever apps escaláveis no React é extrair bons components no momento certo, nada mais.
O que constitui um bom component?
- tamanho, que permite lê-lo sem scrolling
- nome, que indica o que faz
- nenhuma gestão de state irrelevante
- implementação fácil de ler
Quando é hora de dividir um component em components menores?
- quando um component é muito grande
- quando um component executa operações pesadas de gerenciamento de state que podem afetar o desempenho
- quando um component gerencia um state irrelevante
Quais são as regras gerais de composition dos components?
- sempre comece a implementação do topo
- extraia components somente quando você tiver um caso de uso real para ele, não antecipadamente
- sempre comece com os components simples, introduza técnicas avançadas somente quando forem realmente necessárias, não com antecedência
Por hoje é isso, espero que tenham gostado da leitura e achado útil!
Até a próxima ✌🏼
...
Publicado originalmente em https://www.developerway.com. O site tem mais artigos como este. 😉
Assine a newsletter, conecte-se no LinkedIn ou siga no Twitter para ser notificado assim que o próximo artigo for publicado.
Fonte
Artigo escrito por Nadia Makarevich.
Top comments (0)