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

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

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 <select> e <option>), 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 e renderizam dados desse estado.

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

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;

Usando a API

Repare como a API resultante é intuitiva e autodescritiva:

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>
  );
}

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

Avançando: Múltiplos Estados e Flexibilidade

Gerenciando Estados Complexos

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

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;

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:

<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>

Renderização Condicional Implícita

Outro aspecto poderoso é permitir que subcomponentes decidam o que renderizar baseado no estado compartilhado:

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;

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.

Padrões Avançados e Boas Práticas

Composição Customizável

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:

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>
  );
}

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

Validação e Segurança

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

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;
}

Performance e Otimização

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

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>
  );
}

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

Conclusão

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: (1) O padrão permite criar APIs expressivas e autodescritivas, onde a estrutura do código reflete a hierarquia visual final; (2) O contexto implícito elimina prop drilling, tornando a composição escalável mesmo em estruturas complexas; (3) A flexibilidade é fundamental — sempre deixe espaço para customização através de callbacks, validações claras e otimizações de performance com useMemo e useCallback.

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.

Referências


Artigos relacionados