Lección 3 de 15 · Lectura y práctica
Patrones de composición: compound components
CONCEPTO CLAVE: Los Compound Components son un patrón de composición en React donde múltiples componentes trabajan juntos para formar una unidad funcional cohesiva. El componente padre gestiona el estado y la lógica, mientras que.
Del concepto a la práctica
Taller de esta lección
Explicación
Los compound components comparten una unidad de estado mientras ofrecen piezas declarativas. El consumidor decide la estructura, pero el padre controla qué pestaña está activa y cómo se selecciona.
Ejemplo resuelto
<Tabs>
<Tabs.Tab id="catalogo">Catálogo</Tabs.Tab>
<Tabs.Panel id="catalogo">Tres equipos</Tabs.Panel>
</Tabs>El padre puede guardar activa y cada pieza leerla mediante un contexto interno. Así, Tab no necesita recibir manualmente la función desde varios niveles.
Pruébalo
Define un estado inicial
catalogo.Haz que
Tabcambie ese valor al pulsarse.Muestra solo el panel cuyo
idcoincide.
Error frecuente: permitir dos paneles visibles por comparar etiquetas distintas; corrígelo usando un identificador único compartido.
Comprueba tu resultado
Ejercicio: añade pestañas catalogo y reservas. Al seleccionar reservas, el texto visible debe ser solo “Cuatro reservas”; al volver, debe mostrarse solo “Tres equipos”.
Una implementación mínima para completar la API, dentro del módulo que importa React:
const TabsContext = React.createContext(null);
function Tabs({ children }) {
const [activa, seleccionar] = React.useState("catalogo");
return <TabsContext.Provider value={{ activa, seleccionar }}>{children}</TabsContext.Provider>;
}
function useTabs() {
const value = React.useContext(TabsContext);
if (!value) throw new Error("Falta Tabs");
return value;
}
Tabs.Tab = function Tab({ id, children }) {
const { activa, seleccionar } = useTabs();
return <button aria-pressed={activa === id} onClick={() => seleccionar(id)}>{children}</button>;
};
Tabs.Panel = function Panel({ id, children }) {
const { activa } = useTabs();
return activa === id ? <section>{children}</section> : null;
};Este ejemplo usa botones de selección accesibles. Si implementas el patrón ARIA tablist/tab/tabpanel, añade también sus relaciones, foco y navegación por teclado; no basta cambiar nombres de roles.

Mapa de prácticachildren también es una prop: aquí aporta el contenido interior y titulo aporta el encabezado del Panel. El botón Cerrar llama al callback onClose que recibió del padre; el padre decide si mantiene el panel visible.
Ver la infografía en tamaño completo ↗¿Qué son los Compound Components?
Imagina que necesitas crear un componente de Select con opciones. Podrías hacerlo con props:
// Enfoque tradicional con props
Funciona, pero ¿qué pasa cuando necesitas algo más complejo? El patrón Compound Components resuelve esto:
// Enfoque Compound Components
¿Por qué usar Compound Components?
Los beneficios principales son:
- Flexibilidad: El consumidor puede personalizar la estructura del HTML sin romper la funcionalidad.
- Estado compartido: El padre e hijos comparten estado sin necesidad de Context API manual.
- Composición natural: El JSX resultante refleja la estructura visual del componente.
- Reutilización: Los componentes hijos funcionan en diferentes contextos.
Implementación paso a paso
- Crear el contexto: Primero, necesitas un contexto que relacione los componentes hijos con el padre.
// SelectContext.js
import { createContext, useContext, useState } from 'react';
const SelectContext = createContext(null);
export function SelectProvider({ children, value, onChange }) {
return (
{children}
);
}
export function useSelectContext() {
const context = useContext(SelectContext);
if (!context) {
throw new Error('Select components must be used within SelectProvider');
}
return context;
}
- Crear el componente padre: Gestiona el estado y renderiza los hijos.
// Select.jsx
export function Select({ children, onChange }) {
const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen(prev => !prev);
const close = () => setIsOpen(false);
const value = isOpen; // O el valor real seleccionado
return (
<div className="select-container" onBlur={close}>
{children}
</div>
);
}
- Crear los componentes hijos: Cada hijo consume el contexto.
// Select.jsx (continuación)
Select.Trigger = function SelectTrigger({ placeholder }) {
const { value, onChange } = useSelectContext();
return (
<button
className="select-trigger"
onClick={() => onChange(!value)}
>
{placeholder || 'Seleccionar...'}
<span className="arrow">{value ? '▲' : '▼'}</span>
</button>
);
};
Select.Options = function SelectOptions({ children }) {
const { value } = useSelectContext();
if (!value) return null;
return <ul className="select-options">{children}</ul>;
};
Select.Option = function SelectOption({ children, value }) {
const { onChange } = useSelectContext();
return (
<li
className="select-option"
onClick={() => onChange(false)}
>
{children}
</li>
);
};
Object.assign() para añadir propiedades estáticas, es más limpio y mantenible definir cada componente como una propiedad del objeto padre después de definir todos los componentes.Ejemplo práctico completo
Vamos a crear un componente Tabs que demuestra el poder de este patrón:
// Tabs.jsx
import { createContext, useContext, useState } from 'react';
const TabsContext = createContext(null);
export function Tabs({ children, defaultTab }) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">
{children}
</div>
</TabsContext.Provider>
);
}
Tabs.List = function TabsList({ children }) {
return <div className="tabs-list" role="tablist">{children}</div>;
};
Tabs.Tab = function TabsTab({ children, value }) {
const { activeTab, setActiveTab } = useContext(TabsContext);
const isActive = activeTab === value;
return (
<button
role="tab"
aria-selected={isActive}
className={`tab ${isActive ? 'active' : ''}`}
onClick={() => setActiveTab(value)}
>
{children}
</button>
);
};
Tabs.Panel = function TabsPanel({ children, value }) {
const { activeTab } = useContext(TabsContext);
if (activeTab !== value) return null;
return (
<div role="tabpanel" className="tab-panel">
{children}
</div>
);
};
// Uso:
<Tabs defaultTab="react">
<Tabs.List>
<Tabs.Tab value="react">React</Tabs.Tab>
<Tabs.Tab value="vue">Vue</Tabs.Tab>
<Tabs.Tab value="angular">Angular</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="react">Contenido de React...</Tabs.Panel>
<Tabs.Panel value="vue">Contenido de Vue...</Tabs.Panel>
<Tabs.Panel value="angular">Contenido de Angular...</Tabs.Panel>
</Tabs>
Patrones avanzados
Colección implícita
Permite que los hijos se registren automáticamente sin que el padre los conozca:
// Menu.jsx - los items se auto-registran
const MenuContext = createContext(null);
function Menu({ children }) {
const [items, setItems] = useState([]);
const registerItem = (item) => {
setItems(prev => [...prev, item]);
};
return (
<MenuContext.Provider value={{ items, registerItem }}>
<nav className="menu">{children}</nav>
</MenuContext.Provider>
);
}
Menu.Item = function MenuItem({ children, onClick }) {
const { registerItem } = useContext(MenuContext);
useEffect(() => {
registerItem({ onClick, children });
}, []);
return <button onClick={onClick}>{children}</button>;
};
Slots pattern
Permite reemplazar partes específicas del componente:
function Card({ children, slots }) {
return (
<div className="card">
{slots?.header || <default header />}
<div className="card-body">{children}</div>
{slots?.footer}
</div>
);
}
Card.Slot = function CardSlot({ name, children }) {
return { name, children };
};
// Uso
<Card slots={{
header: <Card.Slot name="header">Título personalizado</Card.Slot>,
footer: <Card.Slot name="footer">Acciones</Card.Slot>
}}>
Contenido principal
</Card>
"La composición es la capacidad de combinar funciones simples para construir funciones más complejas. En React, los Compound Components son la manifestación de este principio en la UI." — Kent C. Dodds
Ver más: Ejemplo con Formularios
Los formularios son un caso de uso perfecto para Compound Components:
function Form({ children, onSubmit }) {
const [data, setData] = useState({});
const [errors, setErrors] = useState({});
return (
<FormContext.Provider value={{ data, setData, errors, setErrors }}>
<form onSubmit={handleSubmit(onSubmit)}>{children}</form>
</FormContext.Provider>
);
}
Form.Field = function FormField({ name, label, children, rules }) {
const { errors, setErrors } = useFormContext();
const error = errors[name];
return (
<div className="form-field">
<label>{label}</label>
{children}
{error && <span className="error">{error.message}</span>}
</div>
);
};
Form.Input = function FormInput({ name, ...props }) {
const { data, setData } = useFormContext();
return (
<input
name={name}
value={data[name] || ''}
onChange={e => setData(prev => ({ ...prev, [name]: e.target.value }))}
{...props}
/>
);
};
// Uso
<Form onSubmit={handleLogin}>
<Form.Field name="email" label="Email">
<Form.Input type="email" required />
</Form.Field>
<Form.Field name="password" label="Contraseña">
<Form.Input type="password" required />
</Form.Field>
<button type="submit">Enviar</button>
</Form>
Mejores prácticas
| Práctica | Descripción |
|---|---|
| Validación de contexto | Sempre lanza un error descriptivo si se usa fuera del provider |
| Nombres claros | Usa nombres como Tab.Panel no TabContent |
| Composición vs Props | Usa composición cuando hay múltiples piezas configurables |
| Tipos TypeScript | Exporta tipos para autocomplete del consumidor |
¿Cuál es el propósito principal del patrón Compound Components?
- A) Reducir el número de archivos en el proyecto
- B) Crear componentes flexibles donde el estado se comparte entre padre e hijos mediante un contexto
- C) Eliminar la necesidad de usar props en React
- D) Reemplazar completamente a Redux para gestión de estado
Conclusión
Los Compound Components son un patrón poderoso que te permite crear APIs de componentes más expresivas y flexibles. librerías como Radix UI, Chakra UI y Headless UI utilizan este patrón extensivamente porque:
- Permite separación total entre lógica y presentación
- Hace que los componentes sean más testeables
- Da máxima flexibilidad al consumidor final
- Crea interfaces declarativas que se leen como sentences
Practica refactorizando componentes con muchas props hacia este patrón. Notarás cómo la experiencia de desarrollo mejora drásticamente.
Sigue con React Intermedio: Dominando Componentes y Patrones Avanzados
Esta lección forma parte de un curso de Cursalo. Revisa su programa y elige entre la compra individual o el acceso a toda la escuela.
Si ya tenías acceso a este curso, entra con tu cuenta: tus derechos y tu progreso se conservan. No necesitas comprarlo de nuevo.