React & Frontend

Boas Práticas de Compound Components em React: API Flexível e Contexto Implícito para Times Ágeis

11 min de leitura

Boas Práticas de Compound Components em React: API Flexível e Contexto Implícito para Times Ágeis

O Padrão Compound Components O padrão Compound Components é uma arquitetura de componentização em React que permite criar componentes altamente reutilizáveis e com uma API expressiva. Diferente da abordagem tradicional onde um único componente encapsula toda a lógica, aqui decompomos o componente em partes menores que trabalham juntas, compartilhando estado implicitamente através do Context API. A ideia central é simples: assim como os elementos HTML nativos funcionam juntos (como e ), criamos componentes que se comportam como um "sistema" unificado. O componente pai fornece a lógica e o estado, enquanto os filhos consomem esse contexto automaticamente, sem necessidade de passar props explicitamente em cada nível da árvore. Isso resulta em uma API intuitiva, onde o desenvolvedor escreve código que parece natural. Anatomia e Implementação Básica Entendendo a Estrutura Para dominar Compound Components, precisamos entender três pilares: o componente contenedor (pai), os subcomponentes (filhos) e o contexto implícito que os conecta. O containter gerencia o estado, enquanto os filhos apenas consomem

<h2>O Padrão Compound Components</h2>

<p>O padrão Compound Components é uma arquitetura de componentização em React que permite criar componentes altamente reutilizáveis e com uma API expressiva. Diferente da abordagem tradicional onde um único componente encapsula toda a lógica, aqui decompomos o componente em partes menores que trabalham juntas, compartilhando estado implicitamente através do Context API.</p>

<p>A ideia central é simples: assim como os elementos HTML nativos funcionam juntos (como <code>&lt;select&gt;</code> e <code>&lt;option&gt;</code>), criamos componentes que se comportam como um &quot;sistema&quot; unificado. O componente pai fornece a lógica e o estado, enquanto os filhos consomem esse contexto automaticamente, sem necessidade de passar props explicitamente em cada nível da árvore. Isso resulta em uma API intuitiva, onde o desenvolvedor escreve código que parece natural.</p>

<h2>Anatomia e Implementação Básica</h2>

<h3>Entendendo a Estrutura</h3>

<p>Para dominar Compound Components, precisamos entender três pilares: o componente contenedor (pai), os subcomponentes (filhos) e o contexto implícito que os conecta. O containter gerencia o estado, enquanto os filhos apenas consomem e renderizam dados desse estado.</p>

<p>Vamos construir um exemplo prático: um componente de Accordion reutilizável. Começaremos com a estrutura básica:</p>

<pre><code class="language-jsx">import React, { createContext, useContext, useState } from &#039;react&#039;;

// Criar o contexto que será compartilhado implicitamente

const AccordionContext = createContext();

// Componente pai (contenedor)

function Accordion({ children }) {

const [activeIndex, setActiveIndex] = useState(null);

return (

&lt;AccordionContext.Provider value={{ activeIndex, setActiveIndex }}&gt;

&lt;div className=&quot;accordion&quot;&gt;{children}&lt;/div&gt;

&lt;/AccordionContext.Provider&gt;

);

}

// Hook customizado para acessar o contexto

function useAccordionContext() {

const context = useContext(AccordionContext);

if (!context) {

throw new Error(&#039;useAccordionContext deve ser usado dentro de um Accordion&#039;);

}

return context;

}

// Componente Item

function AccordionItem({ index, children }) {

return (

&lt;div className=&quot;accordion-item&quot; data-index={index}&gt;

{children}

&lt;/div&gt;

);

}

// Componente Trigger (botão que abre/fecha)

function AccordionTrigger({ index, children }) {

const { activeIndex, setActiveIndex } = useAccordionContext();

const isOpen = activeIndex === index;

const handleClick = () =&gt; {

setActiveIndex(isOpen ? null : index);

};

return (

&lt;button

onClick={handleClick}

className={accordion-trigger ${isOpen ? &#039;open&#039; : &#039;&#039;}}

aria-expanded={isOpen}

&gt;

{children}

&lt;/button&gt;

);

}

// Componente Content (conteúdo que expande/colapsa)

function AccordionContent({ index, children }) {

const { activeIndex } = useAccordionContext();

const isOpen = activeIndex === index;

return (

&lt;div

className={accordion-content ${isOpen ? &#039;visible&#039; : &#039;hidden&#039;}}

hidden={!isOpen}

&gt;

{children}

&lt;/div&gt;

);

}

// Exportar como namespace (padrão comum)

Accordion.Item = AccordionItem;

Accordion.Trigger = AccordionTrigger;

Accordion.Content = AccordionContent;

export default Accordion;</code></pre>

<h3>Usando a API</h3>

<p>Repare como a API resultante é intuitiva e autodescritiva:</p>

<pre><code class="language-jsx">function App() {

return (

&lt;Accordion&gt;

&lt;Accordion.Item index={0}&gt;

&lt;Accordion.Trigger index={0}&gt;

O que é React?

&lt;/Accordion.Trigger&gt;

&lt;Accordion.Content index={0}&gt;

React é uma biblioteca JavaScript para construir interfaces com componentes reutilizáveis.

&lt;/Accordion.Content&gt;

&lt;/Accordion.Item&gt;

&lt;Accordion.Item index={1}&gt;

&lt;Accordion.Trigger index={1}&gt;

Como funciona o Virtual DOM?

&lt;/Accordion.Trigger&gt;

&lt;Accordion.Content index={1}&gt;

O Virtual DOM é uma representação em memória do DOM real, permitindo otimizações de renderização.

&lt;/Accordion.Content&gt;

&lt;/Accordion.Item&gt;

&lt;/Accordion&gt;

);

}</code></pre>

<p>O desenvolvedor não precisa entender os detalhes internos. Apenas escreve a estrutura, e o contexto é consumido automaticamente pelos subcomponentes. Sem passar <code>activeIndex</code> ou callbacks manualmente para cada elemento.</p>

<h2>Avançando: Múltiplos Estados e Flexibilidade</h2>

<h3>Gerenciando Estados Complexos</h3>

<p>Conforme as necessidades crescem, precisamos adicionar mais lógica. Vamos estender nosso Accordion para permitir múltiplos itens abertos simultaneamente e adicionar animações:</p>

<pre><code class="language-jsx">import React, { createContext, useContext, useState, useCallback } from &#039;react&#039;;

const AccordionContext = createContext();

function Accordion({ children, allowMultiple = false }) {

const [openItems, setOpenItems] = useState(new Set());

const toggleItem = useCallback((index) =&gt; {

setOpenItems((prev) =&gt; {

const newSet = new Set(prev);

if (newSet.has(index)) {

newSet.delete(index);

} else {

if (!allowMultiple) {

newSet.clear();

}

newSet.add(index);

}

return newSet;

});

}, [allowMultiple]);

const contextValue = {

openItems,

toggleItem,

isOpen: (index) =&gt; openItems.has(index),

};

return (

&lt;AccordionContext.Provider value={contextValue}&gt;

&lt;div className=&quot;accordion&quot; role=&quot;region&quot;&gt;

{children}

&lt;/div&gt;

&lt;/AccordionContext.Provider&gt;

);

}

function useAccordionContext() {

const context = useContext(AccordionContext);

if (!context) {

throw new Error(&#039;useAccordionContext deve ser usado dentro de um Accordion&#039;);

}

return context;

}

function AccordionItem({ index, children }) {

return (

&lt;div className=&quot;accordion-item&quot; data-index={index}&gt;

{children}

&lt;/div&gt;

);

}

function AccordionTrigger({ index, children }) {

const { isOpen, toggleItem } = useAccordionContext();

const open = isOpen(index);

return (

&lt;button

onClick={() =&gt; toggleItem(index)}

className={accordion-trigger ${open ? &#039;open&#039; : &#039;&#039;}}

aria-expanded={open}

aria-controls={content-${index}}

&gt;

&lt;span className=&quot;trigger-text&quot;&gt;{children}&lt;/span&gt;

&lt;span className=&quot;trigger-icon&quot;&gt;{open ? &#039;−&#039; : &#039;+&#039;}&lt;/span&gt;

&lt;/button&gt;

);

}

function AccordionContent({ index, children }) {

const { isOpen } = useAccordionContext();

const open = isOpen(index);

return (

&lt;div

id={content-${index}}

className={accordion-content ${open ? &#039;open&#039; : &#039;closed&#039;}}

role=&quot;region&quot;

hidden={!open}

style={{

maxHeight: open ? &#039;500px&#039; : &#039;0&#039;,

overflow: &#039;hidden&#039;,

transition: &#039;max-height 0.3s ease-in-out&#039;,

}}

&gt;

&lt;div className=&quot;accordion-content-inner&quot;&gt;{children}&lt;/div&gt;

&lt;/div&gt;

);

}

Accordion.Item = AccordionItem;

Accordion.Trigger = AccordionTrigger;

Accordion.Content = AccordionContent;

export default Accordion;</code></pre>

<p>Agora temos uma API ainda mais flexível: permite múltiplos itens abertos, possui melhor acessibilidade com ARIA attributes, e a transição é suave. Tudo isso sem quebrar a simplicidade de uso:</p>

<pre><code class="language-jsx">&lt;Accordion allowMultiple&gt;

&lt;Accordion.Item index={0}&gt;

&lt;Accordion.Trigger index={0}&gt;Seção 1&lt;/Accordion.Trigger&gt;

&lt;Accordion.Content index={0}&gt;Conteúdo 1&lt;/Accordion.Content&gt;

&lt;/Accordion.Item&gt;

&lt;/Accordion&gt;</code></pre>

<h3>Renderização Condicional Implícita</h3>

<p>Outro aspecto poderoso é permitir que subcomponentes decidam o que renderizar baseado no estado compartilhado:</p>

<pre><code class="language-jsx">function AccordionHeader({ index, children }) {

const { isOpen } = useAccordionContext();

return (

&lt;header className={accordion-header ${isOpen(index) ? &#039;expanded&#039; : &#039;collapsed&#039;}}&gt;

{children}

&lt;/header&gt;

);

}

function AccordionIcon({ index }) {

const { isOpen } = useAccordionContext();

const open = isOpen(index);

return &lt;span className=&quot;icon&quot;&gt;{open ? &#039;▼&#039; : &#039;▶&#039;}&lt;/span&gt;;

}

Accordion.Header = AccordionHeader;

Accordion.Icon = AccordionIcon;</code></pre>

<p>Cada subcomponente é independente, mas todos acessam o mesmo estado implicitamente. Se um desenvolvedor quiser adicionar um novo elemento visual que reage ao estado, basta criar um novo subcomponente que consume o contexto.</p>

<h2>Padrões Avançados e Boas Práticas</h2>

<h3>Composição Customizável</h3>

<p>O verdadeiro poder dos Compound Components aparece quando você permite composição customizável. Não force uma estrutura rígida; deixe o desenvolvedor reorganizar os elementos conforme necessário:</p>

<pre><code class="language-jsx">function Accordion({ children, defaultIndex = null, onIndexChange }) {

const [activeIndex, setActiveIndex] = useState(defaultIndex);

const handleChange = (index) =&gt; {

const newIndex = activeIndex === index ? null : index;

setActiveIndex(newIndex);

onIndexChange?.(newIndex);

};

const contextValue = {

activeIndex,

onIndexChange: handleChange,

isOpen: (index) =&gt; activeIndex === index,

};

return (

&lt;AccordionContext.Provider value={contextValue}&gt;

&lt;div className=&quot;accordion&quot;&gt;{children}&lt;/div&gt;

&lt;/AccordionContext.Provider&gt;

);

}</code></pre>

<p>Callbacks opcionais (<code>onIndexChange</code>) permitem ao desenvolvedor integrar o Accordion com sua própria lógica, sem modificar o componente. Essa é flexibilidade real.</p>

<h3>Validação e Segurança</h3>

<p>Sempre valide o contexto e forneça mensagens de erro claras. Isso economiza horas de debugging para quem usa seu componente:</p>

<pre><code class="language-jsx">function useAccordionContext(componentName = &#039;Componente&#039;) {

const context = useContext(AccordionContext);

if (!context) {

throw new Error(

${componentName} deve ser renderizado dentro de um Accordion. +

Certifique-se de que está envolvido por &lt;Accordion&gt;...&lt;/Accordion&gt;

);

}

return context;

}</code></pre>

<h3>Performance e Otimização</h3>

<p>Com contextos, toda mudança de estado causa re-renderização de todos os consumers. Para grandes listas, isso pode ser problemático. Use <code>useMemo</code> e <code>useCallback</code> estrategicamente:</p>

<pre><code class="language-jsx">function Accordion({ children, allowMultiple = false }) {

const [openItems, setOpenItems] = useState(new Set());

const toggleItem = useCallback((index) =&gt; {

setOpenItems((prev) =&gt; {

const newSet = new Set(prev);

if (newSet.has(index)) {

newSet.delete(index);

} else {

if (!allowMultiple) newSet.clear();

newSet.add(index);

}

return newSet;

});

}, [allowMultiple]);

// Memoizar o valor do contexto

const contextValue = useMemo(() =&gt; ({

openItems,

toggleItem,

isOpen: (index) =&gt; openItems.has(index),

}), [openItems, toggleItem]);

return (

&lt;AccordionContext.Provider value={contextValue}&gt;

&lt;div className=&quot;accordion&quot;&gt;{children}&lt;/div&gt;

&lt;/AccordionContext.Provider&gt;

);

}</code></pre>

<p>Sem <code>useMemo</code>, o objeto de contexto seria recriado a cada render, causando re-renderizações desnecessárias de todos os filhos.</p>

<h2>Conclusão</h2>

<p>Compound Components é um padrão sofisticado que combina a potência do Context API com uma arquitetura modular e intuitiva. Os três aprendizados principais são: <strong>(1)</strong> O padrão permite criar APIs expressivas e autodescritivas, onde a estrutura do código reflete a hierarquia visual final; <strong>(2)</strong> O contexto implícito elimina prop drilling, tornando a composição escalável mesmo em estruturas complexas; <strong>(3)</strong> A flexibilidade é fundamental — sempre deixe espaço para customização através de callbacks, validações claras e otimizações de performance com <code>useMemo</code> e <code>useCallback</code>.</p>

<p>Use este padrão quando precisar de componentes altamente reutilizáveis e compostos. Evite quando a lógica é muito simples — nem tudo precisa ser Compound Components.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://react.dev/reference/react/useContext" target="_blank" rel="noopener noreferrer">React Context API - Documentação Oficial</a></li>

<li><a href="https://kentcdodds.com/blog/compound-components-with-react-hooks" target="_blank" rel="noopener noreferrer">Compound Components Pattern - Kent C. Dodds</a></li>

<li><a href="https://react.dev/reference/react" target="_blank" rel="noopener noreferrer">React Hooks Documentation</a></li>

<li><a href="https://frontendmasters.com/courses/advanced-react-patterns/" target="_blank" rel="noopener noreferrer">Advanced Patterns in React - Frontend Masters</a></li>

<li><a href="https://blog.logrocket.com/render-props-vs-compound-component-patterns-in-react/" target="_blank" rel="noopener noreferrer">A Deep Dive into Children in React - LogRocket</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em React & Frontend

Como Usar Redux Toolkit Moderno: Slices, RTK Query e Thunks Tipados em Produção
Como Usar Redux Toolkit Moderno: Slices, RTK Query e Thunks Tipados em Produção

Entendendo Redux Toolkit: Fundamentos e Filosofia Redux é uma biblioteca de g...

Internacionalização em React: react-i18next e Formatação de Dados na Prática
Internacionalização em React: react-i18next e Formatação de Dados na Prática

Entendendo Internacionalização em Aplicações React Internacionalização (i18n)...

Guia Completo de useSyncExternalStore: Integrando Stores Externas com React
Guia Completo de useSyncExternalStore: Integrando Stores Externas com React

O Problema: Estado Externo e React Quando trabalhamos com React, frequentemen...