Skip to Content
DocumentaciónPatronesCompound Components

Compound Components Pattern

Los Compound Components en Creative Minds son una forma de organizar features donde múltiples componentes pequeños trabajan juntos para formar una funcionalidad completa. Imagina que tienes una navbar: en lugar de crear un componente gigante que haga todo, divides la navbar en piezas más pequeñas como Logo, Links, Actions, etc.

¿Por Qué Usar Este Patrón?

El problema que resuelve es simple: reutilización inteligente. Sin este patrón, si necesitas una navbar diferente para admin, tendrías que duplicar código. Con Compound Components, reutilizas las mismas piezas pero las combinas de forma diferente.

¿Cómo Funciona la Arquitectura?

El patrón se estructura en 3 niveles que trabajan juntos:

Nivel 1: Componentes Individuales

Son las piezas básicas como Logo, Links, ThemeToggle. Cada uno hace una cosa específica y puede ser Server Component (estático) o Client Component (interactivo).

Nivel 2: Orquestador (index.tsx)

Es como un “menú” que agrupa todos los componentes individuales y los exporta como un objeto. Aquí defines qué piezas están disponibles para usar.

Nivel 3: Integradores (_feat.tsx)

Son las “recetas” que combinan las piezas del orquestador para crear versiones específicas. Puedes tener navbar_feat.tsx (completa), minimal_feat.tsx (solo logo), admin_feat.tsx (con opciones de admin).

El resultado: Una feature, múltiples versiones, sin duplicar código.


Implementación Paso a Paso

Vamos a construir una navbar completa usando este patrón. Te mostraré cada nivel y cómo se conectan entre sí.

Nivel 1: Componente Root (El Contenedor Inteligente)

El componente Root es especial porque es un Server Component que puede obtener datos del servidor y pasárselos a sus hijos usando render props. Esto es clave porque permite que los componentes hijos accedan a información del servidor sin tener que hacer fetch por su cuenta.

¿Por qué render props? Porque necesitamos pasar datos del servidor a componentes que pueden estar en el cliente. Los render props nos permiten “inyectar” estos datos de forma limpia.

// src/features/navbar/components/root.tsx export async function Root({ children, className, }: { children: (data: { userRole: string; notifications: number }) => React.ReactNode; className?: string; }) { // 🔥 Server Component - Obtiene datos del servidor const userRole = await getUserRole(); const notifications = await getNotificationCount(); return ( <nav className={`navbar ${className || ''}`}> {/* 🔥 Render Props - Pasa datos a los hijos */} {children({ userRole, notifications })} </nav> ); }

Nivel 1: Componentes Individuales (Las Piezas del Rompecabezas)

Ahora creamos los componentes individuales que formarán nuestra navbar. Cada uno tiene una responsabilidad específica y clara. La clave aquí es la separación entre Server y Client Components.

Server Components (sin prefijo): Se ejecutan en el servidor, son perfectos para contenido estático, imágenes, links, etc. Son más rápidos porque no envían JavaScript al cliente.

Client Components (con prefijo _): Se ejecutan en el cliente, necesarios para interactividad como botones, formularios, estados locales. El prefijo _ es una convención de Creative Minds para identificarlos fácilmente.

// src/features/navbar/components/logo.tsx // 🔥 Server Component - Se renderiza en el servidor export function Logo() { return ( <Link href="/" className="flex items-center gap-2"> <Image src="/logo.png" alt="Logo" width={40} height={40} /> <span className="font-bold">Creative Minds</span> </Link> ); } // src/features/navbar/components/links.tsx // 🔥 Server Component con Suspense - Optimizado para carga export function Links() { return ( <Suspense fallback={<div>Loading...</div>}> <NavigationLinks /> </Suspense> ); } // src/features/navbar/components/_theme-toggle.tsx // 🔥 Client Component (prefijo _) - Interactivo 'use client'; export function _ThemeToggle() { const [theme, setTheme] = useState('light'); return ( <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}> {theme === 'light' ? '🌙' : '☀️'} </button> ); }

Nivel 2: Orquestador (El Menú de Componentes)

El archivo index.tsx actúa como un “menú” o “catálogo” de todos los componentes disponibles. No es un barrel file (exportación simple), sino que organiza los componentes como un objeto para crear la API de Compound Components.

Aquí defines qué componentes están disponibles y cómo se agrupan. Por ejemplo, puedes agrupar varios Client Components bajo Actions para simplificar su uso.

// src/features/navbar/index.tsx import { Root } from './components/root'; import { Logo } from './components/logo'; import { Links } from './components/links'; import { _ThemeToggle } from './components/_theme-toggle'; // 🔥 Compound Components - Agrupa todos los componentes export const Navbar = { Root, // Server Component con datos Logo, // Server Component estático Links, // Server Component con Suspense Actions: () => ( // Grupo de Client Components <div className="flex gap-2"> <_ThemeToggle /> <NotificationBell /> </div> ), };

Nivel 3: Integradores (Las Recetas Finales)

Los archivos _feat.tsx son donde combinas las piezas para crear versiones específicas de tu feature. Cada _feat.tsx es como una “receta” que usa los ingredientes (componentes) del orquestador para crear un plato específico (versión de la navbar).

La magia está en que puedes crear múltiples versiones sin duplicar código: una navbar completa para la homepage, una minimalista para landing pages, una especial para el panel de admin, etc.

// src/features/navbar/navbar_feat.tsx // 🔥 Versión COMPLETA - Lista para usar import { Navbar } from './index'; export default function NavbarFeat() { return ( <Navbar.Root className="w-full border-b"> {({ userRole, notifications }) => ( <div className="flex justify-between items-center px-4"> {/* Lado izquierdo */} <div className="flex items-center gap-4"> <Navbar.Logo /> <Navbar.Links /> </div> {/* Lado derecho */} <div className="flex items-center gap-2"> <Navbar.Actions /> {/* 🔥 Usa datos del servidor */} {notifications > 0 && ( <span className="badge">{notifications}</span> )} </div> </div> )} </Navbar.Root> ); } // src/features/navbar/minimal_feat.tsx // 🔥 Versión MINIMALISTA - Solo logo export default function MinimalNavbarFeat() { return ( <Navbar.Root className="py-2"> {() => ( <div className="flex justify-center"> <Navbar.Logo /> </div> )} </Navbar.Root> ); }

Cómo Usar en Tu Aplicación

Una vez que tienes tu feature construida con Compound Components, hay diferentes formas de usarla dependiendo de tus necesidades.

Uso Básico: La Forma Más Simple

La mayoría del tiempo, simplemente importas y usas la versión completa (_feat.tsx). Esto es lo más común y recomendado porque ya está todo integrado y listo para usar.

// app/layout.tsx import NavbarFeat from '@/features/navbar/navbar_feat'; export default function Layout({ children }) { return ( <div> {/* 🔥 Usa la versión completa integrada */} <NavbarFeat /> <main>{children}</main> </div> ); }

Uso Personalizado: Cuando Necesitas Algo Específico

A veces necesitas una versión personalizada que no existe como _feat.tsx. En estos casos, usas directamente los Compound Components del orquestador. Esto te da control total sobre cómo se combinan las piezas.

Es más trabajo, pero te permite crear exactamente lo que necesitas. Perfecto para casos especiales como páginas de admin, landing pages específicas, o cuando necesitas integrar componentes externos.

// app/admin/layout.tsx import { Navbar } from '@/features/navbar'; export default function AdminLayout({ children }) { return ( <div> <Navbar.Root className="admin-navbar"> {({ userRole }) => ( <div className="flex justify-between"> <Navbar.Logo /> {/* 🔥 Navegación personalizada para admin */} <nav className="flex gap-4"> <a href="/admin/users">Users</a> <a href="/admin/settings">Settings</a> </nav> {/* 🔥 Solo acciones, sin links normales */} <Navbar.Actions /> {/* 🔥 Muestra rol del usuario */} <span className="role-badge">{userRole}</span> </div> )} </Navbar.Root> {children} </div> ); }

El Poder de las Múltiples Versiones

Aquí es donde brilla el patrón. Puedes tener una sola feature con múltiples versiones ya preparadas. Cada versión está optimizada para un contexto específico, pero todas reutilizan el mismo código base.

Esto elimina la duplicación de código y hace que mantener diferentes versiones sea mucho más fácil. Si cambias el logo, se actualiza automáticamente en todas las versiones.

// Diferentes páginas usan diferentes versiones import NavbarFeat from '@/features/navbar/navbar_feat'; // Completa import MinimalNavbarFeat from '@/features/navbar/minimal_feat'; // Minimalista import AdminNavbarFeat from '@/features/navbar/admin_feat'; // Admin // Homepage - Versión completa export default function HomePage() { return ( <div> <NavbarFeat /> <main>Home content</main> </div> ); } // Landing - Versión minimalista export default function LandingPage() { return ( <div> <MinimalNavbarFeat /> <main>Landing content</main> </div> ); }

Optimización Avanzada: Partial Pre-Rendering

Una de las ventajas más poderosas de este patrón es que está perfectamente alineado con Partial Pre-Rendering (PPR) de Next.js 15. PPR permite que diferentes partes de tu página se carguen en momentos diferentes para optimizar la velocidad.

Con Compound Components, puedes mezclar estrategias de carga:

  • Server Components estáticos se pre-renderizan (como el logo)
  • Server Components dinámicos se cargan con streaming (como los links que dependen del usuario)
  • Client Components se hidratan después en el cliente (como botones interactivos)

Esto significa que tu navbar puede empezar a mostrarse inmediatamente con el logo, luego cargar los links cuando estén listos, y finalmente activar la interactividad. Todo de forma automática y optimizada.

// app/dashboard/layout.tsx import { Navbar } from '@/features/navbar'; import { Suspense } from 'react'; export default function DashboardLayout({ children }) { return ( <div> <Navbar.Root> {({ userRole }) => ( <div className="dashboard-nav"> {/* 🔥 Pre-renderizado - Estático */} <Navbar.Logo /> {/* 🔥 Streaming - Se carga después */} <Suspense fallback={<div>Loading nav...</div>}> <Navbar.Links /> </Suspense> {/* 🔥 Hidratado - Interactivo en el cliente */} <Navbar.Actions /> </div> )} </Navbar.Root> {children} </div> ); }

Mejores Prácticas

✅ Hacer

// 🔥 Root siempre pasa datos del servidor export async function Root({ children }) { const serverData = await getServerData(); // Datos del servidor return <nav>{children({ data: serverData })}</nav>; } // 🔥 Prefijo _ para Client Components export function _InteractiveButton() { const [clicked, setClicked] = useState(false); return <button onClick={() => setClicked(true)}>Click</button>; } // 🔥 Múltiples versiones para diferentes contextos export default function NavbarFeat() { /* Versión completa */ } export default function MinimalNavbarFeat() { /* Versión simple */ }

❌ Evitar

// ❌ Root sin datos del servidor export function Root({ children }) { return <nav>{children}</nav>; // No pasa datos útiles } // ❌ Client Components sin prefijo _ export function InteractiveButton() { // Debería ser _InteractiveButton const [clicked, setClicked] = useState(false); return <button onClick={() => setClicked(true)}>Click</button>; }

Cuándo Usar

✅ Usar cuando:

  • Necesitas múltiples versiones de la misma feature
  • Quieres reutilizar componentes en diferentes contextos
  • Manejas Server y Client Components juntos
  • Necesitas datos del servidor en los componentes
  • Quieres optimizar para Partial Pre-Rendering

❌ No usar cuando:

  • La feature es muy simple (solo un componente)
  • No necesitas reutilización ni múltiples versiones
  • No manejas datos del servidor

Estructura de Archivos

src/features/navbar/ ├── components/ │ ├── root.tsx # Server Component con render props │ ├── logo.tsx # Server Component │ ├── links.tsx # Server Component + Suspense │ └── _theme-toggle.tsx # Client Component ├── index.tsx # Orquestador (Compound Components) ├── navbar_feat.tsx # Integrador - Versión completa ├── minimal_feat.tsx # Integrador - Versión minimalista └── admin_feat.tsx # Integrador - Versión admin

Resultado: Una feature, múltiples versiones, máxima reutilización y optimización para Next.js 15.

Last updated on