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 adminResultado: Una feature, múltiples versiones, máxima reutilización y optimización para Next.js 15.