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:
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:
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.
Menu.Button = MenuButton
Menu.Dropdown = MenuDropdown
Menu.Item = MenuItem
export default MenuUm 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:
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.
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.
A seguir: Context, a ferramenta que deixa essas peças compartilharem estado não importa onde se sentam.

