Componentes compuestos
Un componente de menú puede escribirse como una caja negra: le pasas buttonText y un array items, y él renderiza todo. Esa versión funciona hasta que deja de funcionar. El usuario final no puede cambiar cómo se renderiza un elemento, no puede reordenar las piezas, no puede reutilizar el botón en otro lado, y cada prop tiene que viajar a través de Menu aunque Menu nunca la use. Los componentes compuestos toman el camino opuesto: varios componentes pequeños diseñados para trabajar juntos, donde el usuario final los compone como si fuera markup.
HTML funciona así desde el principio. Un <select> no es nada sin sus <option>s, un <ul> da estilo a los <li>s que contiene, un <form> rastrea los inputs que envuelve, y la familia de elementos de tabla es todo un ecosistema que solo tiene sentido junto. Los componentes compuestos traen ese contrato padre-e-hijos a tus propios componentes de React.
El problema del prop drilling
El menú monolítico hace prop drilling en miniatura: Menu recibe buttonText e items solo para pasarlos a MenuButton y MenuDropdown. No le importan ninguno de los dos. Ese relé es prop drilling, y levantar estado cubre cuándo una o dos capas de esto es normal y cuándo se estira lo suficiente como para necesitar una herramienta diferente. A través de muchas capas, convierte componentes en mensajeros, y refactorizar cualquier capa intermedia significa reordenar toda la cadena.
A veces la solución correcta es no hacer nada. Hacer drilling uno o dos niveles es más barato que una abstracción prematura, y las abstracciones apresuradas cuestan más que la repetición que eliminan. Los patrones de abajo valen la pena cuando el drilling es real.
Aplanar la estructura
La versión compuesta convierte Menu en un envoltorio que renderiza sus hijos, y promueve las piezas internas al nivel del usuario final:
const sports = ['Tenis', 'Pádel', 'Raquetbol', 'Squash']
<Menu>
<MenuButton>Deportes</MenuButton>
<MenuDropdown>
{sports.map(sport => (
<MenuItem key={sport}>{sport}</MenuItem>
))}
</MenuDropdown>
</Menu>Cada pieza se mantiene pequeña y hace un solo trabajo, y el código detrás de ese markup es prácticamente de una línea:
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 el Button del capítulo de composición. La estructura que antes estaba escondida dentro de Menu ahora es visible en el sitio donde se llama, y los datos van directamente al componente que los usa: el array se mapea justo donde se necesita, sin relé a través de Menu. ¿Quieres que cada elemento sea un link en lugar de texto? Pon un <a> dentro de MenuItem. El autor del componente nunca tiene que anticipar esa solicitud.
El trade-off es transparencia por verbosidad. El usuario final escribe más JSX y obtiene control fino sobre cada capa; la caja negra desaparece en ambos sentidos.
Las piezas son componentes separados, así que el usuario final necesita un import para cada una. Las librerías suelen suavizar eso adjuntando los subcomponentes al padre antes de exportarlo, que es JavaScript puro: las funciones son objetos, así que pueden llevar propiedades.
Menu.Button = MenuButton
Menu.Dropdown = MenuDropdown
Menu.Item = MenuItem
export default MenuUn import trae la familia completa, y el sitio de llamada, <Menu.Button>, anuncia que las piezas pertenecen juntas. La sintaxis de punto es nombrado y nada más. Lo que sea que haga que las piezas trabajen juntas tiene que venir de algún lado, que es la brecha que la siguiente sección abre.
La pieza faltante: estado implícito
El menú aplanado tiene un agujero: el botón ya no abre el dropdown. El booleano abierto/cerrado y su función toggle viven en Menu, y Menu renderiza solo su div envolvente y {children}. No hay ninguna prop siendo pasada a MenuButton, así que no hay lugar donde colgar el toggle. Las piezas necesitan compartir estado sin que el usuario final lo cableé a través de cada etiqueta, lo que la comunidad llama estado implícito: las partes compuestas se coordinan detrás de escenas mientras el usuario final las compone.
React envía una API que puede simular esto. Children.map hace un loop sobre los hijos directos de un componente (props.children no tiene forma garantizada: varios hijos te dan un array, un solo hijo te da el elemento mismo, y ninguno te da undefined, así que llamar .map() en él funciona perfectamente hasta que no funciona), y cloneElement copia un elemento mientras inyecta props extra:
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>
)
}Ahora cada hijo directo de Menu recibe silenciosamente open y toggle, y el botón vuelve a funcionar. También se rompe en el momento en que alguien lo toca, que es la lección real.
Dónde Children.map se queda corto
Hay dos limitaciones, y ambas son estructurales. Primero, es frágil: el mapeo solo toca hijos directos, así que si un usuario final envuelve MenuDropdown en un <div> de estilo, el div recibe toggle y open en lugar de eso, React se queja en la consola de que una función no es un valor válido para un atributo DOM, y el menú deja de funcionar. Un componente compuesto que prohíbe envolver sus piezas ha renunciado a la flexibilidad que la justificó.
Segundo, es limitado en profundidad: MenuItem es un nieto, así que no recibe nada. Hacer que el estado llegue a él significa repetir el baile de Children.map-más-cloneElement dentro de MenuDropdown también, un relé por capa, que es prop drilling disfrazado.
Lo que el patrón realmente necesita es una forma de transportar estado desde Menu a cualquier descendiente, sin importar cuán profundo y sin importar cuántos envoltorios haya. Eso es exactamente lo que context hace, y el capítulo de context toma este mismo menú y lo termina: Menu proporciona { open, toggle } a todo lo que envuelve, y MenuButton lee el toggle con useContext sin importar qué se interponga entre ellos.
Nota de versión
La documentación de React ahora clasifica Children y cloneElement bajo APIs heredadas y recomienda alternativas, context entre ellas, para código nuevo. Todavía funcionan y todavía aparecen en muchas bases de código existentes, por eso vale la pena reconocerlas a primera vista. Las lecciones del curso y la mayoría del código más antiguo las escriben con el import por defecto como React.Children y React.cloneElement.
select y sus options en HTML. Ves la estructura completa justo donde la escribes, y cada pieza obtiene su contenido directamente. El truco es que las piezas todavía necesitan compartir cosas como "¿está abierto el menú?", y el capítulo de context muestra la forma limpia en que lo hacen.
Siguiente: Context, la herramienta que permite que esas piezas compartan estado sin importar cuán profundas estén.

