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.
Esta es una lección del contenido anterior de Cursalo. Si ya estudiaste o compraste este curso, entra con tu cuenta para guardar tu progreso y abrir el resto de las clases. No necesitas comprar nada de nuevo.
¿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.