Next.js
Pages, layouts y templates
Diferencia page, layout y template en el App Router, su alcance, persistencia durante navegación y cuándo conviene reiniciar state y Effects.
- Última actualización
- Actualizada
- Nivel
- Fundamentos
Next.js
Diferencia page, layout y template en el App Router, su alcance, persistencia durante navegación y cuándo conviene reiniciar state y Effects.
page, layout y template no son tres nombres para el mismo wrapper. Una page representa una URL concreta; un layout define UI compartida cuya instancia se preserva durante navegaciones compatibles; un template comparte estructura, pero crea una instancia nueva para reiniciar state y Effects de sus descendientes.
El App Router construye la interfaz mediante composición anidada:
root layout
└─ section layout
└─ template opcional
└─ page o layout descendienteLa ubicación del archivo define su alcance. Un layout en app/dashboard/layout.tsx envuelve /dashboard y todas sus rutas descendientes. Una page en app/dashboard/orders/page.tsx solo representa /dashboard/orders.
La decisión importante es qué UI debe persistir y qué UI debe reiniciarse al navegar.
Sin layouts anidados, cada página tendría que repetir navegación, sidebar, providers y estructura:
function OrdersPage() {
return (
<DashboardShell>
<Orders />
</DashboardShell>
);
}
function CustomersPage() {
return (
<DashboardShell>
<Customers />
</DashboardShell>
);
}Además de duplicar markup, cada navegación podría reconstruir todo el shell y perder estado visual.
Un layout expresa que varias páginas pertenecen a la misma región persistente:
/dashboard/orders
/dashboard/customers
/dashboard/settings
↓
DashboardLayout compartidoUn template resuelve el caso contrario: la estructura se comparte, pero el contenido necesita un ciclo de vida nuevo por navegación.
Una page hace público un segmento y produce la UI específica de la URL:
// app/dashboard/orders/page.tsx
export default async function OrdersPage() {
const orders = await listOrders();
return (
<section>
<h1>Pedidos</h1>
<OrdersTable orders={orders} />
</section>
);
}Una page puede:
params y searchParams.notFound, redirect o una representación vacía.No conviertas cada page en un archivo de cientos de líneas con repositorios, validación y reglas de negocio. La page es un adaptador del router:
URL y request context
↓
page
↓
casos de uso / acceso a datos
↓
view model
↓
componentesUn layout comparte UI entre un segmento y sus descendientes:
// app/dashboard/layout.tsx
import type { ReactNode } from "react";
export default function DashboardLayout({ children }: { children: ReactNode }) {
return (
<div className="dashboard-grid">
<DashboardSidebar />
<main>{children}</main>
</div>
);
}Cuando navegas entre rutas que usan el mismo layout, Next.js conserva su instancia. Esto permite:
La raíz del App Router necesita un layout que defina el documento:
// app/layout.tsx
import type { ReactNode } from "react";
import "./globals.css";
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="es">
<body>{children}</body>
</html>
);
}Debe incluir <html> y <body>. Next.js administra elementos del <head> mediante Metadata API, no mediante un <head> escrito manualmente como patrón general.
app/
├─ layout.tsx
└─ dashboard/
├─ layout.tsx
└─ orders/
├─ layout.tsx
└─ page.tsxPara /dashboard/orders:
RootLayout
└─ DashboardLayout
└─ OrdersLayout
└─ OrdersPageCada nivel puede agregar navegación, providers o contexto visual de su región.
El anidamiento también define el alcance de loading, error y not-found boundaries cercanas.
Imagina un layout cliente:
"use client";
export function CollapsibleSidebar({ children }: { children: React.ReactNode }) {
const [collapsed, setCollapsed] = useState(false);
return (
<div data-collapsed={collapsed}>
<button onClick={() => setCollapsed((value) => !value)}>
Alternar menú
</button>
{children}
</div>
);
}Si vive dentro de un layout preservado, collapsed puede mantenerse al navegar entre páginas descendientes.
Esto no significa que todos los elementos nunca rendericen nuevamente ni que el estado sobreviva una recarga del documento. La persistencia pertenece a la navegación cliente dentro del mismo árbol compatible.
Un layout puede ser asíncrono:
export default async function OrganizationLayout({
children,
params,
}: LayoutProps<"/organizations/[organizationId]">) {
const { organizationId } = await params;
const organization = await getOrganization(organizationId);
return (
<OrganizationShell organization={organization}>
{children}
</OrganizationShell>
);
}Cargar datos en el layout es adecuado cuando todos los descendientes los necesitan. Si solo una page usa el dato, elevarlo amplía el trabajo y acopla toda la sección.
Varias partes pueden solicitar los mismos datos y una capa de caché o memoización del request puede evitar trabajo duplicado. No necesitas necesariamente prop drilling desde el root layout.
Los layouts preservados no reciben automáticamente el pathname actualizado como prop. Esto evita que el servidor tenga que rerenderizar el layout en cada navegación.
Para una UI que depende del pathname, utiliza un Client Component localizado:
"use client";
import { usePathname } from "next/navigation";
export function ActiveNavigation() {
const pathname = usePathname();
return <Navigation currentPath={pathname} />;
}No conviertas todo el layout en cliente si solo un elemento necesita conocer la ruta.
Una page puede recibir searchParams. Un layout no debe depender de ellos como fuente actualizada porque puede permanecer compartido durante navegación.
Una barra de filtros que cambia con la query puede:
useSearchParams en un Client Component.Un template envuelve a sus descendientes como un layout, pero Next.js le asigna una identidad nueva durante navegación:
// app/dashboard/template.tsx
import type { ReactNode } from "react";
export default function DashboardTemplate({ children }: { children: ReactNode }) {
return <div className="route-transition">{children}</div>;
}Conceptualmente:
layout
→ misma instancia entre rutas compatibles
template
→ nueva instancia por navegación relevanteUn wizard o formulario local debe comenzar de nuevo al cambiar de entidad.
Una animación de entrada necesita ejecutarse en cada navegación:
"use client";
export function AnimatedTemplate({ children }: { children: React.ReactNode }) {
useEffect(() => {
trackPageEntered();
}, []);
return <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }}>{children}</motion.div>;
}Colocar esto bajo template crea una nueva instancia y vuelve a ejecutar el Effect.
Una boundary dentro de un layout preservado puede no mostrar el fallback de la misma manera en cada navegación. Un template puede reiniciar su contexto cuando esa UX es intencional.
No lo uses:
key localizada.Remount destruye state, refs, focus y Effects. Esa pérdida debe ser una decisión explícita.
app/documents/[documentId]/
├─ layout.tsx
├─ template.tsx
└─ page.tsxexport default function DocumentsLayout({ children }: { children: React.ReactNode }) {
return (
<DocumentWorkspace>
<DocumentsSidebar />
{children}
</DocumentWorkspace>
);
}El sidebar y su selección general permanecen durante navegación.
export default function DocumentTemplate({ children }: { children: React.ReactNode }) {
return <DocumentDraftBoundary>{children}</DocumentDraftBoundary>;
}Cada documentId crea una nueva instancia del borrador local.
export default async function DocumentPage({
params,
}: PageProps<"/documents/[documentId]">) {
const { documentId } = await params;
const document = await getDocument(documentId);
if (!document) notFound();
return <DocumentEditor initialDocument={document} />;
}Route groups permiten definir árboles con root layouts diferentes:
app/
├─ (marketing)/
│ ├─ layout.tsx
│ └─ page.tsx
└─ (dashboard)/
├─ layout.tsx
└─ dashboard/page.tsxNavegar entre root layouts diferentes puede producir una carga completa del documento. Esto es aceptable para áreas realmente separadas, pero no debe introducirse sin comprender el coste.
Un provider debe colocarse lo más profundo posible sin duplicación innecesaria:
export default function DashboardLayout({ children }: Props) {
return <DashboardPreferencesProvider>{children}</DashboardPreferencesProvider>;
}Un provider cliente obliga a que su propio módulo sea cliente, pero puede recibir Server Components como children. No necesitas convertir todo el contenido descendiente en importaciones cliente.
Un layout puede comprobar sesión para navegación y UX:
const session = await auth();
if (!session) redirect("/login");Pero no constituye la única autorización. Debido a navegación, caché y múltiples entradas, la Data Access Layer y cada mutación deben comprobar permisos cerca de la operación.
Añade estado y dependencias a toda la sección. Mantén el state cerca del consumo o usa una store con límites claros.
El state no se reinicia al cambiar una page descendiente. Usa template o una key específica cuando esa sea la intención.
Una consulta del root afecta todas las rutas. Mueve el acceso a la frontera mínima.
Aumenta la boundary cliente. Extrae únicamente el provider o control interactivo.
Un layout preservado no es el lugar adecuado. Usa un Client Component localizado.
Protege la experiencia visible, no cada operación invocable.
usePathname.Loading, error y not found boundaries explica cómo cada segmento modela espera, fallos inesperados y recursos inexistentes sin derribar todo el árbol.