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)