Rutas paralelas
Las rutas paralelas utilizan carpetas @slot para renderizar múltiples páginas simultáneamente dentro de un único layout. Cada slot es un segmento de ruta independiente con su propio estado de loading, error y navegación.
Busca en todas las páginas de la documentación
Las rutas paralelas utilizan carpetas @slot para renderizar múltiples páginas simultáneamente dentro de un único layout. Cada slot es un segmento de ruta independiente con su propio estado de loading, error y navegación.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Tarjeta de receta de referencia rápida - lista para copiar y pegar.
app/
├── layout.tsx # Recibe @analytics y team como props
├── page.tsx # Contenido principal para /
├── @analytics/
│ ├── page.tsx # Panel de análisis para /
│ ├── default.tsx # Alternativa para sub-rutas sin coincidencia
│ └── loading.tsx # Estado de loading independiente
├── @team/
│ ├── page.tsx # Panel de equipo para /
│ └── default.tsx # Alternativa para sub-rutas sin coincidencia
// app/layout.tsx
export default function Layout({
children,
analytics,
team,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
}) {
return (
<div>
{children}
<div className="grid grid-cols-2 gap-4">
{analytics}
{team}
</div>
</div>
);
}Cuándo usarlo: Dashboards con paneles independientes, contenido condicional basado en autenticación, o cualquier layout que componga múltiples vistas independientes.
// app/dashboard/layout.tsx - Dashboard con tres slots paralelos
export default function DashboardLayout({
children,
metrics,
activity,
}: {
children: React.ReactNode;
metrics: React.ReactNode;
activity: React.ReactNode;
}) {
return (
<div className="p-6">
<h1 className="mb-6 text-2xl font-bold">Dashboard</h1>
<div className="mb-6">{children}</div>
<div className="grid grid-cols-2 gap-6">
<section>
<h2 className="mb-2 text-lg font-semibold">Métricas</h2>
{metrics}
</section>
<section>
<h2 className="mb-2 text-lg font-semibold">Actividad</h2>
{activity}
</section>
</div>
</div>
);
}// app/dashboard/@metrics/page.tsx - Slot de métricas
async function getMetrics() {
const res = await fetch("https://api.example.com/metrics", {
next: { revalidate: 60 },
});
return res.json();
}
export default async function MetricsPanel() {
const metrics = await getMetrics();
return (
<div className="space-y-3 rounded-lg border p-4">
<div className="flex justify-between">
<span>Ingresos</span>
<span className="font-bold">${metrics.revenue.toLocaleString()}</span>
</div>
<div className="flex justify-between">
<span>Usuarios</span>
<span className="font-bold">{metrics.users.toLocaleString()}</span>
</div>
<div className="flex justify-between">
<span>Conversión</span>
<span className="font-bold">{metrics.conversion}%</span>
</div>
</div>
);
}// app/dashboard/@metrics/loading.tsx - Estado de loading independiente
export default function MetricsLoading() {
return (
<div className="space-y-3 rounded-lg border p-4">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="flex justify-between">
<div className="h-4 w-20 animate-pulse rounded bg-gray-200" />
<div className="h-4 w-16 animate-pulse rounded bg-gray-200" />
</div>
))}
</div>
);
}// app/dashboard/@activity/page.tsx - Slot de actividad
async function getActivity() {
const res = await fetch("https://api.example.com/activity", {
next: { revalidate: 30 },
});
return res.json();
}
export default async function ActivityPanel() {
const items = await getActivity();
return (
<ul className="space-y-2 rounded-lg border p-4">
{items.map((item: { id: string; message: string; time: string }) => (
<li key={item.id} className="flex justify-between text-sm">
<span>{item.message}</span>
<span className="text-gray-500">{item.time}</span>
</li>
))}
</ul>
);
}// app/dashboard/@metrics/default.tsx - Alternativa requerida
export default function MetricsDefault() {
return null;
}
// app/dashboard/@activity/default.tsx - Alternativa requerida
export default function ActivityDefault() {
return null;
}// app/dashboard/page.tsx - Contenido principal (se renderiza en {children})
export default function DashboardPage() {
return (
<p className="text-gray-600">
Bienvenido a tu dashboard. Las métricas y la actividad se cargan independientemente a continuación.
</p>
);
}@folder crea un slot con nombre. El prefijo @ le indica a Next.js que pase el slot como prop al layout padre. @metrics se convierte en el prop metrics.children es un slot implícito. El page.tsx en el mismo directorio que el layout automáticamente sirve como slot children. No necesitas crear una carpeta @children.loading.tsx y error.tsx. Cada slot hace streaming y captura errores de forma independiente, por lo que una API lenta no bloquea el renderizado de todo el dashboard.default.tsx proporciona una alternativa. Cuando un slot no tiene una sub-ruta coincidente, default.tsx se renderiza en su lugar. Esto previene errores 404 durante la navegación.@metrics/page.tsx no agrega /metrics a la URL. La URL se determina por los segmentos que no son slots.// Renderizado condicional basado en autenticación
// app/dashboard/layout.tsx
import { auth } from "@/lib/auth";
export default async function DashboardLayout({
children,
admin,
user,
}: {
children: React.ReactNode;
admin: React.ReactNode;
user: React.ReactNode;
}) {
const session = await auth();
const isAdmin = session?.user?.role === "admin";
return (
<div>
{children}
{isAdmin ? admin : user}
</div>
);
}
// app/dashboard/@admin/page.tsx - Se muestra a los administradores
// app/dashboard/@user/page.tsx - Se muestra a los usuarios regulares// Slots con sub-rutas
// app/dashboard/@metrics/detailed/page.tsx
// Navegar a /dashboard/detailed renderiza esto en el slot @metrics
// mientras que @activity muestra su default.tsx (o su propia página /detailed)
export default function DetailedMetrics() {
return <div>Vista de métricas detalladas</div>;
}# Rutas paralelas con navegación independiente
app/dashboard/
├── layout.tsx
├── page.tsx
├── @left/
│ ├── page.tsx # Panel izquierdo por defecto
│ ├── default.tsx
│ └── inbox/page.tsx # /dashboard/inbox → actualiza panel izquierdo
├── @right/
│ ├── page.tsx # Panel derecho por defecto
│ ├── default.tsx
│ └── inbox/page.tsx # /dashboard/inbox → también actualiza panel derecho
// Layout con slots de rutas paralelas
interface DashboardLayoutProps {
children: React.ReactNode; // Slot implícito (page.tsx)
metrics: React.ReactNode; // Slot @metrics
activity: React.ReactNode; // Slot @activity
}
// Las páginas de slots tienen los mismos props que páginas regulares
interface SlotPageProps {
params: Promise<Record<string, string>>;
searchParams: Promise<Record<string, string | string[] | undefined>>;
}
// default.tsx no recibe props
// loading.tsx no recibe props
// error.tsx recibe { error, reset } (debe ser "use client")default.tsx es crítico. Sin él, navegar a una sub-ruta que un slot tiene pero otro no causa un error 404. Siempre crea default.tsx en cada slot.default.tsx. Si no hay default.tsx, Next.js renderiza un 404.@metrics nunca aparece en la URL. No enlaces a /@metrics/something.@slot dentro de otro @slot.@metrics al mismo nivel causarán conflicto.children y los slots nombrados comparten la misma URL. Cuando la URL es /dashboard/settings, Next.js busca /dashboard/settings/page.tsx (children), /dashboard/@metrics/settings/page.tsx, y /dashboard/@activity/settings/page.tsx. Las páginas faltantes recurren a default.tsx.page.tsx y default.tsx. Para tres slots, eso es seis archivos antes de agregar loading y manejo de errores.| Enfoque | Cuándo usar |
|---|---|
Página única con límites <Suspense> | Código más simple cuando la navegación independiente no es necesaria |
| Pestañas o paneles del lado del cliente | Cuando el cambio de panel es puramente visual, no basado en URL |
| Rutas de interceptación | Cuando quieres una superposición modal con una URL compartible |
| Componentes de servidor con streaming | Cuando quieres obtención de datos paralela sin rutas paralelas |
| Grupos de rutas | Organizar rutas sin múltiples vistas simultáneas |
El prefijo @ crea un slot con nombre. Next.js pasa el slot como prop al layout padre. Por ejemplo, @metrics se convierte en el prop metrics en el componente de layout.
El page.tsx en el mismo directorio que el layout automáticamente sirve como slot children. No necesitas crear una carpeta @children.
Navegar a una sub-ruta que un slot tiene pero otro no causa un error 404. En navegación dura (recarga), los slots sin coincidencia sin default.tsx también renderizan un 404. Siempre crea default.tsx en cada slot.
No. Los slots son invisibles en la URL. @metrics/page.tsx no agrega /metrics al camino. No enlaces a /@metrics/something.
Sí. Cada slot hace streaming y captura errores de forma independiente. Una API lenta en un slot no bloquea el renderizado de todo el dashboard.
// app/dashboard/layout.tsx
import { auth } from "@/lib/auth";
export default async function Layout({
children,
admin,
user,
}: {
children: React.ReactNode;
admin: React.ReactNode;
user: React.ReactNode;
}) {
const session = await auth();
const isAdmin = session?.user?.role === "admin";
return (
<div>
{children}
{isAdmin ? admin : user}
</div>
);
}No. Todos los slots deben ser hijos directos del directorio del layout. Los slots anidados no son compatibles.
interface DashboardLayoutProps {
children: React.ReactNode;
metrics: React.ReactNode;
activity: React.ReactNode;
}Cada slot se tipifica como React.ReactNode.
Cuando la URL es /dashboard/settings, Next.js busca:
/dashboard/settings/page.tsx (children)/dashboard/@metrics/settings/page.tsx/dashboard/@activity/settings/page.tsxLas páginas faltantes en un slot recurren a default.tsx de ese slot.
Sí. Cuando los slots se resuelven en diferentes momentos, el layout puede saltar. Usa dimensiones fijas o placeholders esqueléticos con alturas consistentes para prevenir cambios de layout.
Cada slot necesita como mínimo page.tsx y default.tsx. Para tres slots, eso son seis archivos antes de agregar loading.tsx y error.tsx.
Ambos no reciben props. Son componentes simples:
// default.tsx
export default function Default() {
return null;
}
// loading.tsx
export default function Loading() {
return <div>Cargando...</div>;
}Revisado por Chris St. John·Última actualización: 19 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥