<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><select></code> e <code><option></code>), 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.</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 'react';
// Criar o contexto que será compartilhado implicitamente
const AccordionContext = createContext();
// Componente pai (contenedor)
function Accordion({ children }) {
const [activeIndex, setActiveIndex] = useState(null);
return (
<AccordionContext.Provider value={{ activeIndex, setActiveIndex }}>
<div className="accordion">{children}</div>
</AccordionContext.Provider>
);
}
// Hook customizado para acessar o contexto
function useAccordionContext() {
const context = useContext(AccordionContext);
if (!context) {
throw new Error('useAccordionContext deve ser usado dentro de um Accordion');
}
return context;
}
// Componente Item
function AccordionItem({ index, children }) {
return (
<div className="accordion-item" data-index={index}>
{children}
</div>
);
}
// Componente Trigger (botão que abre/fecha)
function AccordionTrigger({ index, children }) {
const { activeIndex, setActiveIndex } = useAccordionContext();
const isOpen = activeIndex === index;
const handleClick = () => {
setActiveIndex(isOpen ? null : index);
};
return (
<button
onClick={handleClick}
className={accordion-trigger ${isOpen ? 'open' : ''}}
aria-expanded={isOpen}
>
{children}
</button>
);
}
// Componente Content (conteúdo que expande/colapsa)
function AccordionContent({ index, children }) {
const { activeIndex } = useAccordionContext();
const isOpen = activeIndex === index;
return (
<div
className={accordion-content ${isOpen ? 'visible' : 'hidden'}}
hidden={!isOpen}
>
{children}
</div>
);
}
// 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 (
<Accordion>
<Accordion.Item index={0}>
<Accordion.Trigger index={0}>
O que é React?
</Accordion.Trigger>
<Accordion.Content index={0}>
React é uma biblioteca JavaScript para construir interfaces com componentes reutilizáveis.
</Accordion.Content>
</Accordion.Item>
<Accordion.Item index={1}>
<Accordion.Trigger index={1}>
Como funciona o Virtual DOM?
</Accordion.Trigger>
<Accordion.Content index={1}>
O Virtual DOM é uma representação em memória do DOM real, permitindo otimizações de renderização.
</Accordion.Content>
</Accordion.Item>
</Accordion>
);
}</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 'react';
const AccordionContext = createContext();
function Accordion({ children, allowMultiple = false }) {
const [openItems, setOpenItems] = useState(new Set());
const toggleItem = useCallback((index) => {
setOpenItems((prev) => {
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) => openItems.has(index),
};
return (
<AccordionContext.Provider value={contextValue}>
<div className="accordion" role="region">
{children}
</div>
</AccordionContext.Provider>
);
}
function useAccordionContext() {
const context = useContext(AccordionContext);
if (!context) {
throw new Error('useAccordionContext deve ser usado dentro de um Accordion');
}
return context;
}
function AccordionItem({ index, children }) {
return (
<div className="accordion-item" data-index={index}>
{children}
</div>
);
}
function AccordionTrigger({ index, children }) {
const { isOpen, toggleItem } = useAccordionContext();
const open = isOpen(index);
return (
<button
onClick={() => toggleItem(index)}
className={accordion-trigger ${open ? 'open' : ''}}
aria-expanded={open}
aria-controls={content-${index}}
>
<span className="trigger-text">{children}</span>
<span className="trigger-icon">{open ? '−' : '+'}</span>
</button>
);
}
function AccordionContent({ index, children }) {
const { isOpen } = useAccordionContext();
const open = isOpen(index);
return (
<div
id={content-${index}}
className={accordion-content ${open ? 'open' : 'closed'}}
role="region"
hidden={!open}
style={{
maxHeight: open ? '500px' : '0',
overflow: 'hidden',
transition: 'max-height 0.3s ease-in-out',
}}
>
<div className="accordion-content-inner">{children}</div>
</div>
);
}
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"><Accordion allowMultiple>
<Accordion.Item index={0}>
<Accordion.Trigger index={0}>Seção 1</Accordion.Trigger>
<Accordion.Content index={0}>Conteúdo 1</Accordion.Content>
</Accordion.Item>
</Accordion></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 (
<header className={accordion-header ${isOpen(index) ? 'expanded' : 'collapsed'}}>
{children}
</header>
);
}
function AccordionIcon({ index }) {
const { isOpen } = useAccordionContext();
const open = isOpen(index);
return <span className="icon">{open ? '▼' : '▶'}</span>;
}
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) => {
const newIndex = activeIndex === index ? null : index;
setActiveIndex(newIndex);
onIndexChange?.(newIndex);
};
const contextValue = {
activeIndex,
onIndexChange: handleChange,
isOpen: (index) => activeIndex === index,
};
return (
<AccordionContext.Provider value={contextValue}>
<div className="accordion">{children}</div>
</AccordionContext.Provider>
);
}</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 = 'Componente') {
const context = useContext(AccordionContext);
if (!context) {
throw new Error(
${componentName} deve ser renderizado dentro de um Accordion. +
Certifique-se de que está envolvido por <Accordion>...</Accordion>
);
}
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) => {
setOpenItems((prev) => {
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(() => ({
openItems,
toggleItem,
isOpen: (index) => openItems.has(index),
}), [openItems, toggleItem]);
return (
<AccordionContext.Provider value={contextValue}>
<div className="accordion">{children}</div>
</AccordionContext.Provider>
);
}</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><!-- FIM --></p>