Layouts
Los Layouts envuelven páginas con UI compartida que persiste entre navegaciones. Nunca se remontan — usa plantillas cuando necesites estado fresco.
Receta
Tarjeta de referencia rápida — lista para copiar y pegar.
// app/layout.tsx — Root layout (requerido, exactamente uno)
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="es">
<body>
<header>Encabezado del sitio</header>
<main>{children}</main>
<footer>Pie de página del sitio</footer>
</body>
</html>
);
}
// app/dashboard/layout.tsx — Nested layout para /dashboard/*
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="flex">
<nav className="w-64">Barra lateral</nav>
<section className="flex-1">{children}</section>
</div>
);
}Cuándo usarlo: Siempre que múltiples páginas compartan la misma UI envolvente — barras de navegación, barras laterales, proveedores o contenedores estructurales.
Ejemplo Funcional
// app/layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: { default: "Mi Aplicación", template: "%s | Mi Aplicación" },
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="es">
<body className={inter.className}>
<nav className="border-b px-6 py-3">
<a href="/">Inicio</a>
<a href="/dashboard" className="ml-4">Panel de control</a>
</nav>
{children}
</body>
</html>
);
}// app/dashboard/layout.tsx
import Link from "next/link";
const sidebarLinks = [
{ href: "/dashboard", label: "Resumen" },
{ href: "/dashboard/analytics", label: "Analítica" },
{ href: "/dashboard/settings", label: "Configuración" },
];
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="flex min-h-screen">
<aside className="w-64 border-r p-4">
<h2 className="mb-4 font-bold">Panel de control</h2>
<ul className="space-y-2">
{sidebarLinks.map((link) => (
<li key={link.href}>
<Link href={link.href} className="text-blue-600 hover:underline">
{link.label}
</Link>
</li>
))}
</ul>
</aside>
<main className="flex-1 p-6">{children}</main>
</div>
);
}// app/dashboard/page.tsx
export default function DashboardPage() {
return <h1>Resumen del Panel de control</h1>;
}
// app/dashboard/analytics/page.tsx
export default function AnalyticsPage() {
return <h1>Analítica</h1>;
// Navegar aquí mantiene la barra lateral montada — sin remount, sin pérdida de estado
}Inmersión Profunda
Cómo Funciona
- El root layout es obligatorio. Debe contener las etiquetas
<html>y<body>. Next.js genera un error si falta este archivo. - Los Layouts reciben solo
children. El propchildrenes la página actual o el siguiente layout anidado en el árbol. - Los Layouts no se remontan en la navegación. React los reconcilia — el estado de componentes, efectos y DOM persisten. Por eso las barras laterales y barras de navegación permanecen interactivas.
- Los Layouts se anidan automáticamente.
app/layout.tsxenvuelveapp/dashboard/layout.tsxque envuelveapp/dashboard/page.tsx. Nunca los compones manualmente. - Los Layouts son Server Components por defecto. Agrega
"use client"solo cuando el layout en sí necesita hooks o APIs del navegador. - Los metadatos se pueden definir por layout. Cada layout (o página) puede exportar un objeto
metadatao funcióngenerateMetadataque se fusiona con los metadatos del padre.
Variaciones
// Template — se remontan en cada navegación (útil para animaciones de entrada/salida)
// app/dashboard/template.tsx
"use client";
import { motion } from "framer-motion";
export default function DashboardTemplate({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
{children}
</motion.div>
);
}// Layout con params — accediendo a params de ruta en un layout
// app/blog/[slug]/layout.tsx
export default async function BlogPostLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return (
<article>
<div className="text-sm text-gray-500">Post: {slug}</div>
{children}
</article>
);
}// Layout condicional — UI diferente según estado de autenticación
// app/dashboard/layout.tsx
import { auth } from "@/lib/auth";
import { redirect } from "next/navigation";
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const session = await auth();
if (!session) redirect("/login");
return (
<div>
<p>Bienvenido, {session.user.name}</p>
{children}
</div>
);
}Notas sobre TypeScript
// Tipo de props del layout
interface LayoutProps {
children: React.ReactNode;
params: Promise<Record<string, string>>; // async en Next.js 15+
}
// Template tiene los mismos props que layout
interface TemplateProps {
children: React.ReactNode;
}
// Tipos de metadatos
import type { Metadata, ResolvingMetadata } from "next";
export async function generateMetadata(
{ params }: { params: Promise<{ slug: string }> },
parent: ResolvingMetadata
): Promise<Metadata> {
const { slug } = await params;
return { title: slug };
}Trampas
- No puedes pasar datos del layout a los children vía props. Los Layouts solo reciben
children. Usa React Context, cookies o una búsqueda de datos compartida en su lugar. - El root layout no puede ser un Client Component sin
<html>y<body>. Incluso como Client Component, debe renderizar esas etiquetas. - Las plantillas y layouts pueden coexistir. El orden de renderizado es
layout.tsx>template.tsx>page.tsx. La plantilla se sitúa entre ellos. - Los Layouts no pueden acceder a
searchParams. Solopage.tsxrecibesearchParams. Si un layout necesita params de consulta, usauseSearchParams()en un hijo Client Component. - Eliminar un layout afecta todas las rutas secundarias. Si borras
app/dashboard/layout.tsx, todas las páginas del panel de control pierden ese envolvente al instante. paramsson asíncronos en Next.js 15+. Siempreawait paramsen layouts — el patrón de desestructuración síncrona está deprecado.
Alternativas
| Enfoque | Cuándo Usar |
|---|---|
template.tsx | Necesitas remount de componente en cada navegación (animaciones, logging) |
| Route Groups con layouts separados | Diferentes layouts para diferentes secciones en la misma profundidad de URL |
| Proveedor de contexto del lado del cliente | Compartir estado entre páginas sin un envolvente visual |
Parallel Routes (@slot) | Renderizar múltiples páginas lado a lado en un layout |
Ejemplo del Mundo Real
De una aplicación SaaS de producción con Next.js 15 / React 19 (SystemsArchitect.io).
Root Layout con Anidamiento de Proveedores
// Ejemplo de producción: Root layout con proveedores y metadatos
// Archivo: app/layout.tsx
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import { ThemeProvider } from '@/components/theme-provider';
import { ToastProvider } from '@/components/toast-provider';
import { ProjectStoreInitializer } from '@/components/project-store-initializer';
import './globals.css';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: { default: 'SystemsArchitect', template: '%s | SystemsArchitect' },
description: 'Plataforma de aprendizaje de arquitectura en la nube',
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="es" suppressHydrationWarning>
<body className={inter.className}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<ToastProvider>
<ProjectStoreInitializer>
{children}
</ProjectStoreInitializer>
</ToastProvider>
</ThemeProvider>
</body>
</html>
);
}Página de Inicio Asíncrona
// Ejemplo de producción: Página de inicio con datos del servidor
// Archivo: app/page.tsx
import { getFeaturedContent } from '@/lib/content';
export default async function HomePage() {
const featured = await getFeaturedContent();
return (
<main>
<h1>Bienvenido a SystemsArchitect</h1>
{featured.map((item) => (
<article key={item.id}>{item.title}</article>
))}
</main>
);
}Lo que esto demuestra en producción:
- El orden de anidamiento de proveedores importa.
ThemeProviderenvuelve todo porque las notificaciones toast y el inicializador de almacén ambos necesitan contexto de tema.ToastProviderviene antes deProjectStoreInitializerporque los errores de inicialización del almacén necesitan mostrar mensajes toast. suppressHydrationWarningen la etiqueta<html>es requerido cuando se usan proveedores de tema que establecen un atributoclassodata-themeen el servidor. Sin él, la desincronización de hidratación entre renderizado del servidor y del cliente desencadena una advertencia de React.- Los Layouts nunca se remontan en la navegación. Esto significa que
ThemeProvider,ToastProvidereProjectStoreInitializerse inicializan una vez y persisten en todas las transiciones de página. Cualquier estado en estos proveedores se conserva. - El patrón
metadata.title.template('%s | SystemsArchitect') permite a las páginas secundarias establecer solo su título (p. ej.,export const metadata = { title: 'Panel de control' }) y se renderiza automáticamente como "Panel de control | SystemsArchitect". - La página de inicio es un Server Component asíncrono. Obtiene datos en tiempo de solicitud con cero JavaScript del cliente. Este patrón reemplaza
getServerSidePropsdel Pages Router.
Preguntas Frecuentes
¿Por qué se requiere el root layout?
El root app/layout.tsx debe existir y debe contener las etiquetas <html> y <body>. Next.js genera un error si falta este archivo porque define la estructura del documento HTML para toda la aplicación.
¿Se remontan los layouts al navegar entre rutas secundarias?
No. Los Layouts persisten entre navegaciones. React los reconcilia para que el estado de componentes, efectos y DOM se conserven. Por eso las barras laterales y barras de navegación permanecen interactivas.
¿Cuál es la diferencia entre layout.tsx y template.tsx?
layout.tsxpersiste y no se remontas en la navegacióntemplate.tsxse remontas en cada navegación, dando estado fresco- El orden de renderizado es:
layout.tsx>template.tsx>page.tsx - Pueden coexistir en el mismo segmento
Trampa: ¿Puedes pasar datos de un layout a sus children vía props?
No. Los Layouts solo reciben children como prop. Usa React Context, cookies o una búsqueda de datos compartida para compartir datos entre un layout y sus páginas secundarias.
¿Pueden los layouts acceder a searchParams?
No. Solo page.tsx recibe searchParams. Si un layout necesita params de consulta, usa useSearchParams() en un hijo Client Component.
¿Cómo accedes a los params de ruta en un layout?
// app/blog/[slug]/layout.tsx
export default async function BlogLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return (
<article>
<p>Post: {slug}</p>
{children}
</article>
);
}¿Cómo funciona la plantilla de título de metadatos?
Establece title: { default: "Mi Aplicación", template: "%s | Mi Aplicación" } en el root layout. Las páginas secundarias exportan metadata = { title: "Panel de control" } y se renderiza como "Panel de control | Mi Aplicación".
Trampa: ¿Qué pasa si los params se desestructuran sincrónicamente en Next.js 15+?
Se rompe. params ahora es una Promise en Next.js 15+. Debes await params en layouts. El patrón síncrono está deprecado.
¿Cuáles son los tipos de TypeScript para props de layout y template?
interface LayoutProps {
children: React.ReactNode;
params: Promise<Record<string, string>>;
}
interface TemplateProps {
children: React.ReactNode;
}¿Cómo escribes generateMetadata en un layout?
import type { Metadata, ResolvingMetadata } from "next";
export async function generateMetadata(
{ params }: { params: Promise<{ slug: string }> },
parent: ResolvingMetadata
): Promise<Metadata> {
const { slug } = await params;
return { title: slug };
}¿Puede el root layout ser un Client Component?
Sí, pero aún debe renderizar las etiquetas <html> y <body>. Incluso como Client Component, estas etiquetas son requeridas.
¿Cómo se anidan automáticamente los layouts?
app/layout.tsx envuelve app/dashboard/layout.tsx que envuelve app/dashboard/page.tsx. Nunca los compones manualmente. Next.js maneja el anidamiento según la jerarquía del sistema de archivos.
Relacionado
- App Router Basics — descripción general de convenciones de archivo
- Route Groups — múltiples layouts en el mismo nivel
- Parallel Routes — segmentos @slot en layouts
- Loading & Error — límites de carga y error dentro de layouts