Criando Componentes Flexíveis com Compound Pattern e Composition

Published: (December 6, 2025 at 09:31 AM EST)
4 min read
Source: Dev.to

Source: Dev.to

O Problema: A Abordagem Rígida

Imagine que você precisa criar um Accordion. A abordagem iniciante geralmente centraliza tudo na configuração:

// ❌ O jeito rígido (Componente Monolítico)

O problema aqui é que a configuração (data) está misturada com a UI. Se o designer decidir que o título do segundo item precisa ser vermelho, você terá que “sujar” seu código com mais props condicionais.

A Solução: Compound Components + Acessibilidade

O padrão Compound Component permite que componentes trabalhem juntos, compartilhando estado implicitamente, enquanto a Composition deixa o desenvolvedor decidir onde renderizar cada parte.

Além disso, ao criarmos nossos próprios componentes, temos a responsabilidade de torná‑los acessíveis. Um Accordion sem atributos ARIA (aria-expanded, aria-controls) é invisível para usuários que dependem de leitores de tela.

Queremos chegar neste resultado (DX - Developer Experience):

// ✅ O jeito flexível e acessível

<div>
  <h2>O que é React?</h2>
  <p>Uma biblioteca JavaScript para criar interfaces...</p>
</div>

<div>
  <h3>Acessibilidade</h3>
  <button>Saiba mais</button>
</div>

Mão na Massa: Implementando o Padrão

Vamos construir isso usando React Context, Hooks e useId para garantir a acessibilidade.

1. Criando o Contexto Principal

Primeiro, o estado global do Accordion.

import React, { createContext, useContext, useState, ReactNode, useId } from "react";

type AccordionContextType = {
  openItem: string | null;
  toggleItem: (value: string) => void;
};

const AccordionContext = createContext<AccordionContextType | undefined>(undefined);

const useAccordion = () => {
  const context = useContext(AccordionContext);
  if (!context) {
    throw new Error("Os componentes do Accordion devem estar dentro de ");
  }
  return context;
};

2. O Componente Pai (Root)

interface AccordionProps {
  children: ReactNode;
}

const AccordionRoot = ({ children }: AccordionProps) => {
  const [openItem, setOpenItem] = useState<string | null>(null);

  const toggleItem = (value: string) => {
    setOpenItem(prev => (prev === value ? null : value));
  };

  return (
    <AccordionContext.Provider value={{ openItem, toggleItem }}>
      {children}
    </AccordionContext.Provider>
  );
};

3. Os Sub‑Componentes Inteligentes

O Item (Gerenciador de IDs)

// Contexto para passar IDs e Valor para os filhos (Trigger e Content)
const ItemContext = createContext<any>(undefined);

const AccordionItem = ({
  value,
  children,
}: {
  value: string;
  children: ReactNode;
}) => {
  const id = useId(); // Gera um ID único para este item
  const triggerId = `accordion-trigger-${id}`;
  const contentId = `accordion-content-${id}`;

  return (
    <ItemContext.Provider value={{ value, triggerId, contentId }}>
      {children}
    </ItemContext.Provider>
  );
};

O Gatilho (Trigger)

const AccordionTrigger = ({ children }: { children: ReactNode }) => {
  const { openItem, toggleItem } = useAccordion();
  const itemContext = useContext(ItemContext);

  if (!itemContext) throw new Error("Trigger deve estar dentro de um Item");

  const { value, triggerId, contentId } = itemContext;
  const isOpen = openItem === value;

  return (
    <button
      id={triggerId}
      aria-controls={contentId}
      aria-expanded={isOpen}
      onClick={() => toggleItem(value)}
      className="flex justify-between w-full p-4 font-medium text-left hover:bg-gray-50 transition focus:outline-none focus:ring-2 focus:ring-blue-500"
    >
      {children}
      {isOpen ? "➖" : "➕"}
    </button>
  );
};

O Conteúdo (Content)

const AccordionContent = ({ children }: { children: ReactNode }) => {
  const { openItem } = useAccordion();
  const itemContext = useContext(ItemContext);

  if (!itemContext) throw new Error("Content deve estar dentro de um Item");

  const { value, triggerId, contentId } = itemContext;
  const isOpen = openItem === value;

  if (!isOpen) return null;

  return (
    <div
      id={contentId}
      role="region"
      aria-labelledby={triggerId}
      className="p-4"
    >
      {children}
    </div>
  );
};

4. Finalizando a Composição

export const Accordion = Object.assign(AccordionRoot, {
  Item: AccordionItem,
  Trigger: AccordionTrigger,
  Content: AccordionContent,
});

Por que a Acessibilidade Importa?

Quando criamos componentes customizados (“na unha”), perdemos a semântica nativa do HTML (como as tags <details> e <summary>). Ao adicionar aria-expanded e aria-controls, garantimos que usuários que navegam via teclado ou leitores de tela tenham a mesma experiência que usuários visuais. Um componente bonito que não pode ser usado por todos é um componente incompleto.

Mas e o <details> e <summary>?

Você deve estar se perguntando: “Por que escrever tanto código se o HTML5 já tem as tags <details> e <summary>?”

O uso de tags nativas é sempre encorajado, pois garante acessibilidade e SEO out‑of‑the‑box. No entanto, criar um Accordion controlado via React (como fizemos acima) é necessário quando:

  • Exclusividade de Abertura – Queremos que, ao abrir um item, os outros se fechem automaticamente. Fazer isso com <details> exige interceptar eventos e manipular o atributo open manualmente, o que nos traz de volta ao gerenciamento de estado.
  • Animações Fluidas – Animar o fechamento de um elemento nativo <details> ainda é complexo no CSS puro (devido à transição de height: auto). Com React podemos controlar a renderização e aplicar animações CSS ou bibliotecas de animação de forma previsível.
Back to Blog

Related posts

Read more »