La composición sobre la herencia — Construir interfaces flexibles mediante la composición de componentes en lugar de extenderlos
Receta
// Composición basada en slots: pasa componentes como props
function Layout({ header, sidebar, children }: {
header: React.ReactNode;
sidebar: React.ReactNode;
children: React.ReactNode;
}) {
return (
<div className="grid grid-cols-[240px_1fr] grid-rows-[64px_1fr]">
<header className="col-span-2">{header}</header>
<aside>{sidebar}</aside>
<main>{children}</main>
</div>
);
}
// Uso
<Layout
header={<TopNav user={currentUser} />}
sidebar={<SideMenu items={menuItems} />}
>
<Dashboard data={dashboardData} />
</Layout>Cuándo usarlo: Siempre que necesites un contenedor, diseño o contenedor reutilizable que deba permanecer agnóstico sobre su contenido. Prefiere la composición sobre la herencia en todos los casos en React.
Ejemplo funcionando
import { type ReactNode } from "react";
// Un componente Card con slots componibles
interface CardProps {
media?: ReactNode;
actions?: ReactNode;
children: ReactNode;
variant?: "elevated" | "outlined";
}
function Card({ media, actions, children, variant = "outlined" }: CardProps) {
const base = "rounded-lg overflow-hidden";
const styles = variant === "elevated"
? `${base} shadow-lg bg-white`
: `${base} border border-gray-200 bg-white`;
return (
<div className={styles}>
{media && <div className="aspect-video overflow-hidden">{media}</div>}
<div className="p-4">{children}</div>
{actions && (
<div className="px-4 pb-4 flex gap-2 justify-end">{actions}</div>
)}
</div>
);
}
// Especialización mediante composición, NO herencia
function ProductCard({ product }: { product: Product }) {
return (
<Card
variant="elevated"
media={<img src={product.image} alt={product.name} />}
actions={
<>
<button className="btn-secondary">Guardar</button>
<button className="btn-primary">Añadir al carrito</button>
</>
}
>
<h3 className="font-semibold">{product.name}</h3>
<p className="text-gray-600">${product.price}</p>
</Card>
);
}
interface Product {
name: string;
price: number;
image: string;
}Lo que esto demuestra:
- Patrón de slot mediante props con nombre
ReactNode(media,actions,children) - Especialización envolviendo un componente genérico (
Card) dentro de uno específico del dominio (ProductCard) - Cero herencia —
ProductCardno es una subclase deCard
Inmersión profunda
Cómo funciona
- Los componentes de React son funciones que devuelven JSX. No hay ninguna jerarquía de clases para extender.
childrenes la primitiva de composición principal — cualquier JSX anidado dentro de un componente se pasa comoprops.children.- Los slots con nombre (props tipadas como
ReactNode) te permiten inyectar contenido en posiciones específicas dentro del diseño de un componente. - La especialización se logra creando un nuevo componente que renderiza un componente general con props específicas pre-rellenas.
- La contención se logra mediante componentes que no conocen sus children de antemano y usan
{children}como marcador de posición.
Parámetros y valores de retorno
| Patrón de prop | Tipo | Propósito |
|---|---|---|
children | ReactNode | Slot predeterminado para contenido anidado |
Slot con nombre (p. ej. header) | ReactNode | Colocación explícita de contenido en un área específica |
| Callback de renderizado | (data: T) => ReactNode | Contenido que necesita acceso al estado interno (véase render-props) |
| Prop de componente | React.ComponentType<P> | Inyectar un componente completo para instanciar internamente |
Variaciones
Patrón de inyección de componentes — pasa un tipo de componente en lugar de un elemento renderizado:
interface ListProps<T> {
items: T[];
renderItem: React.ComponentType<{ item: T }>;
}
function List<T>({ items, renderItem: Item }: ListProps<T>) {
return (
<ul>
{items.map((item, i) => (
<li key={i}><Item item={item} /></li>
))}
</ul>
);
}Composición de proveedores — aplana los proveedores de contexto anidados:
function AppProviders({ children }: { children: ReactNode }) {
return (
<ThemeProvider>
<AuthProvider>
<QueryProvider>
{children}
</QueryProvider>
</AuthProvider>
</ThemeProvider>
);
}Notas de TypeScript
- Usa
ReactNodepara slots que aceptan cualquier contenido renderizable (strings, elementos, fragmentos,null). - Usa
React.ComponentType<P>cuando necesites pasar un componente que será instanciado con props específicas. - Usa
ReactElementsolo cuando necesites limitar a elementos JSX reales (excluyendo strings y números). - La composición genérica (
List<T>) preserva la seguridad de tipos en todo el límite del componente.
Trampas
-
Sobreuso de
childrenpara múltiples slots — Cuando pasas todo comochildren, pierdes el control sobre la colocación. Solución: Usa props con nombreReactNodepara áreas de contenido distintas. -
Prop drilling a través de capas compuestas — La composición profunda puede llevar a pasar props a través de muchos niveles. Solución: Usa contexto para preocupaciones realmente transversales y mantén la composición superficial.
-
Romper la memoización con slots JSX en línea — Pasar
<Component />en línea como una prop crea una nueva referencia de elemento en cada renderizado. Solución: Eleva el contenido de slot estático fuera del renderizado o envuelve conuseMemopara árboles costosos. -
Confundir especialización con configuración — Crear un nuevo componente solo para establecer algunas props está bien; crear un contenedor que re-expone todas las props originales es una señal de que necesitas composición, no envolvimiento. Solución: Usa el componente original directamente y pasa props en el sitio de llamada.
Alternativas
| Enfoque | Compensación |
|---|---|
| Composición (slots) | Más flexible; requiere más JSX en el sitio de llamada |
| Props de configuración (strings de variante) | Menos flexible pero API más simple para casos comunes |
| Render props | Más poder cuando el contenido del slot necesita estado interno |
| Componentes de orden superior | Añade comportamiento sin cambiar la API, pero más difícil de depurar |
| Herencia | No recomendado en React — se rompe con componentes de función |
Preguntas frecuentes
¿Cuál es la diferencia entre composición e herencia en React?
- Los componentes de React son funciones, no clases, por lo que no hay jerarquía de clases para extender.
- Composición significa construir interfaces anidando y combinando componentes en lugar de crear relaciones de subclase.
- La especialización se logra envolviendo un componente general con props específicas pre-rellenas, no extendiéndolo.
- React recomienda oficialmente la composición sobre la herencia en todos los casos.
¿Cuándo debes usar props de slot con nombre en lugar de solo children?
- Usa props con nombre
ReactNode(comoheader,sidebar,actions) cuando el contenido necesita colocarse en posiciones específicas dentro de un diseño. - Usa
childrensolo cuando hay un único área de contenido. - Los slots con nombre te dan control explícito sobre la colocación que
childrensolo no puede proporcionar.
¿Cómo difiere el patrón de inyección de componentes de pasar elementos renderizados como props?
// Elemento renderizado: pasas JSX
<Card media={<img src={url} alt="photo" />} />
// Inyección de componentes: pasas un tipo de componente
<List items={data} renderItem={ProductRow} />- Con la inyección de componentes, el componente padre instancia el componente inyectado internamente y controla qué props recibe.
- Con elementos renderizados, el consumidor controla la instanciación y los valores de props.
¿Cómo aplanar proveedores de contexto profundamente anidados usando composición?
function AppProviders({ children }: { children: ReactNode }) {
return (
<ThemeProvider>
<AuthProvider>
<QueryProvider>
{children}
</QueryProvider>
</AuthProvider>
</ThemeProvider>
);
}- Crea un único componente contenedor que anida todos los proveedores.
- Los consumidores solo ven
<AppProviders>en el sitio de llamada.
¿Cuál es la diferencia entre ReactNode, ReactElement, y React.ComponentType<P> en TypeScript?
ReactNodeacepta cualquier contenido renderizable: strings, números, elementos, fragmentos ynull.ReactElementse limita solo a elementos JSX reales, excluyendo strings y números.React.ComponentType<P>es un tipo de componente (función o clase) que puede instanciarse con props de tipoP.
¿Cómo preserva la composición genérica como List<T> la seguridad de tipos?
interface ListProps<T> {
items: T[];
renderItem: React.ComponentType<{ item: T }>;
}
function List<T>({ items, renderItem: Item }: ListProps<T>) {
return <ul>{items.map((item, i) => <li key={i}><Item item={item} /></li>)}</ul>;
}- TypeScript infiere
Tdel arrayitemsy lo aplica enrenderItem. - El consumidor obtiene autocompletado completo en la prop
itemdentro del componente de renderizado.
Trampa: ¿Por qué pasar JSX en línea como una prop de slot rompe la memoización?
- Pasar
<Component />en línea como una prop crea una nueva referencia de elemento React en cada renderizado. - Si el componente receptor usa
React.memo, la nueva referencia causa que se re-renderice. - Solución: eleva el contenido de slot estático fuera de la función de renderizado o envuelve con
useMemo.
Trampa: ¿Cuándo crear un componente contenedor se convierte en un anti-patrón?
- Si tu contenedor re-expone todas las props originales sin añadir comportamiento, tienes una capa innecesaria.
- Esto es configuración, no composición. Solo usa el componente original directamente y pasa props en el sitio de llamada.
- Los contenedores son apropiados cuando pre-rellenan props específicas para crear una especialización específica del dominio.
¿Por qué no se recomienda la herencia en React?
- El modelo de componentes de React se basa en funciones y composición, no en jerarquías de clases.
- Los componentes de función no pueden extenderse mediante herencia en absoluto.
- Incluso con componentes de clase, el equipo de React nunca ha encontrado un caso de uso donde la herencia sea preferible a la composición.
¿Cuándo debes usar una prop de callback de renderizado en lugar de un slot ReactNode?
- Usa un callback de renderizado (
(data: T) => ReactNode) cuando el contenido del slot necesita acceso al estado interno del componente padre. - Usa un slot
ReactNodesimple cuando el contenido es independiente del estado interno del componente. - Los callbacks de renderizado añaden flexibilidad pero también complejidad. Comienza con
ReactNodey actualiza solo cuando sea necesario.
¿Cómo tipas un componente que acepta tanto children como slots con nombre en TypeScript?
interface LayoutProps {
header: ReactNode;
sidebar: ReactNode;
children: ReactNode;
}
function Layout({ header, sidebar, children }: LayoutProps) {
return (
<div>
<header>{header}</header>
<aside>{sidebar}</aside>
<main>{children}</main>
</div>
);
}- Todas las props de slot están tipadas como
ReactNode. childrenes una prop estándar de React que recibe JSX anidado.
Relacionado
- Render Props — Cuando los slots necesitan acceso al estado interno del componente
- Compound Components — Componentes de múltiples partes que comparten estado implícito
- Context Patterns — Evitar prop drilling en árboles profundamente compuestos