Skip to content
This page has been auto-translated and may contain errors.View in English

Componentes compostos

Um componente de menu pode ser escrito como uma caixa preta: você passa buttonText e um array items, e ele renderiza tudo sozinho. Essa versão funciona até deixar de funcionar. Quem chama o componente não consegue mudar como um item é renderizado, não consegue reordenar as peças, não consegue reutilizar o botão em outro lugar, e toda prop tem que passar por Menu mesmo quando Menu nunca a usa. Componentes compostos fazem o oposto: vários componentes pequenos projetados para trabalhar juntos, e quem chama os compõe como se fossem marcação HTML.

HTML tem funcionado assim desde o início. Um <select> não é nada sem seus <option>s, um <ul> estiliza os <li>s dentro dele, um <form> rastreia os inputs que envolve, e a família de elementos de tabela é um ecossistema inteiro que só faz sentido junto. Componentes compostos trazem esse contrato entre pai e filhos para seus próprios componentes React.

O problema da passagem de props

O menu monolítico passa props em miniatura: Menu recebe buttonText e items só para repassá-los a MenuButton e MenuDropdown. Não se importa com nenhum deles. Esse repasse é passagem de props, e elevar o estado cobre quando uma ou duas camadas disso é normal e quando se estende o suficiente para precisar de outra ferramenta. Ao longo de muitas camadas, transforma componentes em mensageiros, e refatorar qualquer camada do meio significa refazer toda a corrente.

Às vezes a resposta certa é não fazer nada. Passar props por um ou dois níveis é mais barato que uma abstração precipitada, e abstrações feitas de forma apressada custam mais do que a repetição que removem. Os padrões abaixo compram seu lugar quando a passagem de props é realmente um problema.

Achatando a estrutura

A versão composta transforma Menu em um invólucro que renderiza seus filhos, e promove as peças internas para o nível de quem está chamando:

jsx
const sports = ['Tênis', 'Pickleball', 'Raquetebol', 'Squash']

<Menu>
  <MenuButton>Esportes</MenuButton>
  <MenuDropdown>
    {sports.map(sport => (
      <MenuItem key={sport}>{sport}</MenuItem>
    ))}
  </MenuDropdown>
</Menu>

Cada peça permanece pequena e faz um trabalho, e o corpo por trás dessa marcação são praticamente uma linha:

jsx
function Menu({ children }) {
  return <div className="menu">{children}</div>
}

function MenuButton({ children }) {
  return <Button>{children}</Button>
}

function MenuDropdown({ children }) {
  return <div className="menu-dropdown">{children}</div>
}

function MenuItem({ children }) {
  return <div className="menu-item">{children}</div>
}

MenuButton reutiliza o Button do capítulo de composição. A estrutura que costumava estar escondida dentro de Menu agora está visível no local onde é chamada, e os dados vão direto para o componente que os usa: o array é mapeado bem onde é necessário, sem passar por Menu. Quer que cada item seja um link em vez de texto? Coloque um <a> dentro de MenuItem. O autor do componente nunca precisa antecipar esse pedido.

A troca é transparência por verbosidade. Quem chama escreve mais JSX e ganha controle fino sobre cada camada; a caixa preta se foi nos dois sentidos.

As peças são componentes separados, então quem chama precisa de um import para cada uma. Bibliotecas geralmente suavizam isso anexando os subcomponentes ao pai antes de exportá-lo, o que é JavaScript puro: funções são objetos, então podem carregar propriedades.

jsx
Menu.Button = MenuButton
Menu.Dropdown = MenuDropdown
Menu.Item = MenuItem
export default Menu

Um import traz toda a família, e o local de chamada, <Menu.Button>, deixa claro que as peças pertencem juntas. A sintaxe de ponto é apenas nomenclatura. Seja lá o que faz as peças funcionarem juntas tem que vir de algum lugar, e esse é o vazio que a próxima seção abre.

A peça que falta: estado implícito

O menu achatado tem um buraco nele: o botão não abre mais o dropdown. O boolean aberto/fechado e sua função de alternância vivem em Menu, e Menu renderiza apenas seu div envolvente e {children}. Não há prop sendo passada a MenuButton, então não há lugar para pendurar a alternância. As peças precisam compartilhar estado sem quem chama ter que conectá-lo a cada tag, o que a comunidade chama de estado implícito: as partes compostas se coordenam nos bastidores enquanto quem chama as compõe.

React fornece uma API que pode fingir isso. Children.map percorre os filhos diretos de um componente (o props.children não tem um formato garantido: vários filhos lhe dão um array, um filho único lhe dá o elemento em si, e nenhum lhe dá undefined, então chamar .map() nele funciona até não funcionar mais), e cloneElement copia um elemento enquanto injeta props extras:

jsx
import { Children, cloneElement, useState } from 'react'

function Menu({ children }) {
  const [open, setOpen] = useState(false)
  const toggle = () => setOpen(prevOpen => !prevOpen)

  return (
    <div className="menu">
      {Children.map(children, child =>
        cloneElement(child, { open, toggle })
      )}
    </div>
  )
}

Agora todo filho direto de Menu silenciosamente recebe open e toggle, e o botão funciona de novo. Também se quebra no momento que alguém respira nele, e é essa a lição real.

Onde Children.map não é suficiente

Há dois problemas, e ambos são estruturais. Primeiro, é frágil: o mapeamento só toca filhos diretos, então se quem chama envolve MenuDropdown em uma <div> de estilo, a div recebe toggle e open em vez disso, React reclama no console que uma função não é um valor válido para um atributo DOM, e o menu para de funcionar. Um componente composto que proíbe envolver suas peças abriu mão da flexibilidade que o justificava.

Segundo, é limitado em profundidade: MenuItem é um neto, então não recebe nada. Para passar estado a ele, é preciso repetir a dança de Children.map mais cloneElement dentro de MenuDropdown também, um repasse por camada, que é passagem de props com fantasia.

O que o padrão realmente precisa é de um jeito de teletransportar estado de Menu para qualquer descendente, não importa quanto profundo e não importa o que esteja envolvido. Exatamente isso que context faz, e o capítulo de context pega esse mesmo menu e o termina: Menu fornece { open, toggle } a tudo que envolve, e MenuButton lê a alternância com useContext não importa o que estiver entre eles.

Nota de versão

Os docs do React agora listam Children e cloneElement como APIs legado e recomendam alternativas, context entre elas, para código novo. Ainda funcionam e ainda aparecem em muito código existente, que é por que vale a pena reconhecê-las à primeira vista. As lições do curso e a maioria do código mais antigo as escrevem com o import padrão como React.Children e React.cloneElement.

Decidir se um componente deve ser composto depende de quem precisa controlar o meio. Um componente cuja layout interna é fixa e improvável mudar é melhor monolítico com alguns props; o padrão composto se paga quando quem chama continua pedindo uma opção de renderização a mais e a lista de props está virando uma linguagem de configuração. Widgets de disclosure, menus, abas e acordeões vivem nessa zona, é por isso que toda biblioteca headless mainstream os fornece em forma composta.

Fornecer uma família tem dois custos que vale a pena precificar. Anexar as peças ao pai as amarra para o bundler, então importar Menu puxa todo subcomponente se a página renderiza ou não, e uma peça raramente usada segue na chunk de qualquer forma.

O segundo custo é que uma API composta pode descrever sua estrutura esperada sem nunca impô-la: nada impede quem chama de renderizar MenuDropdown sem MenuButton acima dele, e policiar isso comparando o type de um filho contra a identidade do componente quebra assim que um invólucro se senta no meio ou duas cópias do módulo aterram em uma build. A versão que aguenta documenta a forma pretendida e faz cada peça se comportar sensatamente por si só quando a forma está quebrada.

JunoPequenas peças que funcionam juntas Em vez de um componente Menu grande que leva um monte de props e renderiza tudo, componentes compostos lhe dão várias tags pequenas, um Menu, um MenuButton, um MenuDropdown, um MenuItem, que você arranja você mesmo do jeito que arranja um select e suas options em HTML. Você vê a estrutura inteira bem onde a escreve, e cada peça recebe seu conteúdo diretamente.

O problema é as peças ainda precisam compartilhar coisas como "o menu está aberto", e o capítulo de context mostra o jeito limpo de fazer isso.

JunoPequenas peças que funcionam juntas Recorra a componentes compostos quando os props de um único componente estão virando uma linguagem de configuração: exponha as peças, deixe quem chama as compor, e mapeie dados no local de chamada em vez de repassá-los através de um invólucro.

Children.map com cloneElement pode injetar estado compartilhado em filhos diretos, mas quebra sob uma div envolvente e nunca alcança netos, então trate como um degrau intermediário e use context para a coordenação real.

JunoPequenas peças que funcionam juntas Componentes compostos trocam uma API opaca de componente único por uma transparente e componível; faça a troca quando quem chama precisa controlar as camadas intermediárias, e pule quando a layout é fixa.

Coordene as peças através de um context provider com escopo em vez de Children.map e cloneElement, que são APIs legado, frágeis sob envolvimento, e limitadas em profundidade por projeto.

Fornecer as peças como uma família tem um preço: anexá-las ao pai impede o bundler de soltar as que uma página nunca renderiza, e a API pode documentar sua estrutura esperada sem nunca impô-la.

A seguir: Context, a ferramenta que deixa essas peças compartilharem estado não importa onde se sentam.