Estructura del App Router
El App Router usa el directorio app/ con convenciones de archivos especiales:
| Archivo | Propósito |
|---|---|
layout.tsx | Layout compartido (persiste entre navegaciones) |
page.tsx | UI única de la ruta |
loading.tsx | UI de carga (Suspense boundary) |
error.tsx | Error boundary |
not-found.tsx | UI para 404 |
Layouts anidados
Los layouts envuelven a sus hijos y persisten entre navegaciones:
// app/layout.tsx — Layout raíz
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="es">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<nav>
<a href="/">Inicio</a>
<a href="/blog">Blog</a>
</nav>
<main>{children}</main>
</body>
</html>
);
}
// app/blog/layout.tsx — Layout del blog
export default function BlogLayout({ children }: { children: React.ReactNode }) {
return (
<div className="blog-container">
<aside>Sidebar del blog</aside>
<section>{children}</section>
</div>
);
}
Al navegar entre páginas del blog, el layout raíz y el layout del blog se mantienen montados — solo cambia el page.tsx.
Pages
Cada page.tsx define la UI de una ruta específica:
// app/page.tsx
export default function HomePage() {
return <h1>Página principal</h1>;
}
// app/blog/page.tsx
export default function BlogPage() {
return <h1>Listado del blog</h1>;
}
Rutas dinámicas
// app/blog/[slug]/page.tsx
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return (
<article>
<h1>Post: {slug}</h1>
<a href="/blog">Volver al blog</a>
</article>
);
}
En Vinext (como en Next.js 15+),
paramses una Promise. Puedes usarawaitpara obtener los valores.
generateStaticParams
Pre-renderiza rutas dinámicas en build time:
export const dynamicParams = true;
export async function generateStaticParams() {
return [
{ slug: "primer-post" },
{ slug: "segundo-post" },
];
}
Loading States
loading.tsx crea un Suspense boundary automático:
// app/blog/loading.tsx
export default function BlogLoading() {
return (
<div className="loading">
<p>Cargando artículos...</p>
</div>
);
}
Mientras el page.tsx del blog resuelve sus datos asíncronos, el usuario ve el loading state. Vinext soporta streaming SSR: el HTML del loading se envía inmediatamente y el contenido real se inyecta cuando está listo.
Error Boundaries
error.tsx captura errores en tiempo de ejecución:
// app/blog/error.tsx
"use client";
export default function BlogError({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>Error en el blog</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>Reintentar</button>
</div>
);
}
error.tsxdebe ser un Client Component ("use client") porque usaonClick.
Not Found
// app/not-found.tsx
export default function NotFound() {
return (
<div>
<h2>404 - Página no encontrada</h2>
<a href="/">Volver al inicio</a>
</div>
);
}
Route Groups
Los route groups (nombre) organizan rutas sin afectar la URL:
app/
(marketing)/
layout.tsx # Layout para marketing
blog/page.tsx # /blog
about/page.tsx # /about
(shop)/
layout.tsx # Layout para tienda
products/page.tsx # /products
cart/page.tsx # /cart
Ambos grupos comparten el layout raíz pero tienen layouts internos diferentes. Las URLs no incluyen el nombre del grupo.
Parallel Routes
Los parallel routes renderizan múltiples páginas simultáneamente en el mismo layout:
app/dashboard/
layout.tsx # Recibe { children, metrics, activity }
page.tsx # children
@metrics/
page.tsx # Slot de métricas
default.tsx # Fallback
@activity/
page.tsx # Slot de actividad
default.tsx # Fallback
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
metrics,
activity,
}: {
children: React.ReactNode;
metrics: React.ReactNode;
activity: React.ReactNode;
}) {
return (
<div className="dashboard-grid">
<div className="main">{children}</div>
<div className="sidebar">
{metrics}
{activity}
</div>
</div>
);
}
Cada slot (@metrics, @activity) tiene su propio loading.tsx, error.tsx y navegación independiente.
Catch-All y Optional Catch-All
app/docs/[...slug]/page.tsx # /docs/a, /docs/a/b, /docs/a/b/c
app/shop/[[...slug]]/page.tsx # /shop, /shop/a, /shop/a/b
// app/docs/[...slug]/page.tsx
export default async function DocsPage({
params,
}: {
params: Promise<{ slug: string[] }>;
}) {
const { slug } = await params;
return <h1>Docs: {slug.join("/")}</h1>;
}
Siguiente paso
En el siguiente capítulo profundizaremos en SSR y React Server Components: streaming, directivas y la arquitectura multi-ambiente de Vinext.