Patrones de composición: compound components

Lectura
30 min~6 min lectura
Objetivo de la lección

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.

Puntos de control
  • ¿Qué son los Compound Components?
  • ¿Por qué usar Compound Components?
  • Implementación paso a paso
  • Ejemplo práctico 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

  
  
    React
    Vue
    Angular
  

📌 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"
        {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"
      => 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"
      => 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' : ''}`}
      => 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
};

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
    </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] || ''}
      => setData(prev => ({ ...prev, [name]: e.target.value }))}
      {...props}
    />
  );
};

// Uso
<Form
  <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.

Laboratorio de práctica

Antes de marcar esta lección como completa, escribí una evidencia breve para React Intermedio: Dominando Componentes y Patrones Avanzados: un ejemplo, una decisión, una captura, una mini demo o una nota que puedas reutilizar en portfolio.

Reflexión rápida

¿Qué cambiarías en tu forma de trabajar después de aplicar patrones de composición: compound components?

De lección a portfolio

Convertí esta lección en una prueba técnica visible.

Una app pequeña publicada, con README y decisiones explicadas, funciona mejor que una lista de tecnologías sueltas.

Paso 1

Creá una demo mínima que use el concepto de la lección.

Paso 2

Escribí un README corto con objetivo, stack, decisión técnica y mejora futura.

Paso 3

Publicá la demo y enlazala desde tu perfil profesional.

Newsletter Cursalo

Recibí rutas y cursos nuevos

Sumate para recibir recursos orientados a empleo y portfolio.

  • Rutas de empleo
  • Cursos prácticos
  • Portfolio y entrevistas

Sin spam. También podés entrar con tu cuenta para guardar progreso. Iniciá sesión

Patrones de composición: compound components | CursaloFalar no WhatsApp