Saltar al contenido

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

jsx
<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

  1. Define un estado inicial catalogo.

  2. Haz que Tab cambie ese valor al pulsarse.

  3. Muestra solo el panel cuyo id coincide.

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:

jsx
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.

El Padre envía titulo Reserva y children al Panel; el botón Cerrar notifica al Padre mediante onClose.
CursaloMapa de práctica
Props y children tienen papeles distintos

children 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 ↗
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 los componentes hijos exponen una API declarativa e intuitiva para el desarrollador que los consume.

¿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

📌 Este patrón es exactamente lo que usa Radix UI, Headless UI y @radix-ui/react-select para crear componentes accesibles sin opinión sobre el estilo.

¿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

  1. 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;
}
  1. 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>
    
  );
}
  1. 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>
  );
};
💡 Tip: Aunque puedes usar 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>
⚠️ Advertencia: No confundas Compound Components con Control Props Pattern. En Compound Components, el estado pertenece al padre y los hijos solo lo consumen. No deben intentar modificar el estado directamente (salvo que sea intencional con callbacks).

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
📌 Recuerda: Los Compound Components shine cuando necesitas un componente que sea Flexible en su uso, Intuitivo en su API, y Consistente en su comportamiento.
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ácticaDescripción
Validación de contextoSempre lanza un error descriptivo si se usa fuera del provider
Nombres clarosUsa nombres como Tab.Panel no TabContent
Composición vs PropsUsa composición cuando hay múltiples piezas configurables
Tipos TypeScriptExporta tipos para autocomplete del consumidor
💡 Pro tip: Si tu componente necesita más de 5 props de configuración, considera si Compound Components podría simplificar la API.
🧠 Quiz

¿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
✅ Respuesta correcta: B. El patrón Compound Components permite crear componentes flexibles y declarativos donde el componente padre provee un contexto que los hijos consumen, compartiendo estado sin props drilling.

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.

01 / ELEGIR UNA RUTA

El próximo paso

Aprende IA mientras construyes algo que sí necesitas.

Empieza por el curso inicial o explora el catálogo para encontrar una práctica alineada con tu trabajo.