Parallel Routes en Next.js: Cómo Construir Layouts con Estados de Carga Independientes
Aprende a construir layouts complejos con Parallel Routes e Intercepting Routes en Next.js. Estados de carga independientes para UI resiliente.
El 90% de los Layouts Complejos en Next.js Fracasan Por Estados de Carga Acoplados
Vuestra aplicación tiene tres secciones principales: sidebar de navegación, feed de contenido, y panel de analytics.
Cada sección carga datos de APIs distintas. Un usuario hace scroll en el feed. La API del sidebar peta. Todo se congela. Spinner global. Pantalla en blanco.
Esto os suena. A todos.
*El problema real no es que una API falle. Es que vuestra UI no está diseñada para fallar de forma aislada.*
Parallel Routes resuelven esto. Cada slot carga, falla, y se recupera de forma independiente. Sin afectar al resto de la página.
La sabiduría convencional asume que layouts complejos requieren estados de carga sincronizados para mantener la consistencia visual. En realidad, ese acoplamiento es la principal causa de experiencias de usuario catastróficas.
El 95% de los sistemas con validación human-in-the-loop alcanza correctness en recuperación de errores. El secreto: aislamiento por capas. Cada componente puede fallar y reintentar sin colapsar todo el sistema.
Parallel Routes aplican este principio a nivel de UI. Es como tener microservicios para componentes.
Por Qué Vuestros Loading States Están Destinados al Fracaso
Suspense de React os permite crear loading states. Pero Suspense tradicional acoplado significa: cuando un componente suspende, toda la sección padre se queda en espera.
Imaginad un dashboard con cinco widgets. Cada widget consume una API distinta. Un solo widget lento hace que todo el dashboard muestre un skeleton loader genérico durante segundos.
El usuario ve una interfaz rota. No sabe qué cargó. No sabe qué falló. No puede interactuar con nada.
❌ Enfoque acoplado:
Una API lenta bloquea toda la UI
Un error en un widget muestra pantalla de error global
Loading states genéricos que no comunican progreso real
Imposible reintentar una sección sin refrescar toda la página
✅ Enfoque con Parallel Routes:
Cada slot tiene su propio loading.tsx independiente
Un error en @analytics muestra el error solo en ese slot
El resto de la página sigue siendo interactiva
Reintento granular por sección sin afectar a las demás
El 90% de las migraciones de Supabase fracasan por priorizar cambios inmediatos sobre validación robusta. Parallel Routes os obligan a pensar en validación por capas desde el diseño inicial.
Cómo Funciona la Arquitectura de Slots Paralelos
La convención es simple: carpetas con prefijo @ en app/.
```
app/
├── layout.tsx # Layout principal
├── @analytics/
│ ├── page.tsx # Contenido del slot
│ ├── loading.tsx # Loading específico
│ └── error.tsx # Error handling específico
├── @notifications/
│ ├── page.tsx
│ ├── loading.tsx
│ └── error.tsx
├── @feed/
│ ├── page.tsx
│ ├── loading.tsx
│ └── error.tsx
├── page.tsx # Combina todos los slots
└── globals.css
```
En page.tsx principal, referenciáis cada slot como propiedades:
```tsx
// app/page.tsx
export default function DashboardPage() {
return (
<div className="dashboard-grid">
<aside className="sidebar">
<slot id="@analytics" />
</aside>
<main className="feed">
<slot id="@feed" />
</main>
<aside className="notifications">
<slot id="@notifications" />
</aside>
</div>
)
}
```
El layout principal define la estructura. Cada slot se renderiza de forma independiente.
Loading states granulares con loading.tsx
Cada slot puede tener su propio loading.tsx con feedback específico:
```tsx
// app/@analytics/loading.tsx
export default function AnalyticsLoading() {
return (
<div className="animate-pulse space-y-4">
<div className="h-8 w-32 bg-gray-200 rounded" />
<div className="grid grid-cols-2 gap-4">
<div className="h-24 bg-gray-200 rounded" />
<div className="h-24 bg-gray-200 rounded" />
</div>
<div className="h-64 bg-gray-200 rounded" />
</div>
)
}
```
Este skeleton solo aparece si @analytics suspende o está cargando. Los otros slots (@feed, @notifications) mantienen su estado aunque @analytics esté cargando.
Error handling isolate con error.tsx
```tsx
// app/@analytics/error.tsx
'use client'
import { useEffect } from 'react'
export default function AnalyticsError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
console.error('Analytics slot error:', error)
}, [error])
return (
<div className="p-4 border border-red-200 rounded-lg bg-red-50">
<p className="text-sm text-red-800 font-medium">
Error cargando analytics
</p>
<button
onClick={reset}
className="mt-2 text-sm text-red-600 hover:text-red-800 underline"
>
Reintentar
</button>
</div>
)
}
```
El slot @analytics muestra este error. Pero @feed y @notifications siguen funcionando. El usuario puede seguir navegando mientras el equipo de analytics resuelve su problema.
Intercepting Routes: Modales Sin Perder el Contexto
Intercepting Routes permiten renderizar rutas en contextos diferentes sin cambiar la URL visible.
Caso de uso: click en un producto abre un modal con detalles. Pero si el usuario accede directamente a la URL del producto, se renderiza la página completa.
```
app/
├── @analytics/
├── @notifications/
├── producto/
│ ├── page.tsx # Página completa
│ └── @modal/
│ └── default.tsx # Modal interceptado
└── layout.tsx
```
En @modal/default.tsx:
```tsx
// app/producto/@modal/default.tsx
import Image from 'next/image'
import { obtenerProducto } from '@/lib/api'
export default async function ProductoModal({
params,
}: {
params: { id: string }
}) {
const producto = await obtenerProducto(params.id)
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="bg-white rounded-xl p-6 max-w-2xl w-full mx-4">
<div className="flex gap-6">
<div className="relative w-48 h-48">
<Image
src={producto.imagen}
alt={producto.nombre}
fill
className="object-cover rounded-lg"
/>
</div>
<div className="flex-1">
<h2 className="text-2xl font-bold">{producto.nombre}</h2>
<p className="mt-2 text-gray-600">{producto.descripcion}</p>
<p className="mt-4 text-3xl font-bold text-blue-600">
{producto.precio}
</p>
</div>
</div>
</div>
</div>
)
}
```
La lógica de Next.js determina automáticamente: si el usuario llegó vía interceptación (clics en la UI), muestra el modal. Si accedió directamente a /producto/123, muestra page.tsx completo.
Sincronización de estado entre slots paralelos
Cuando un slot necesita reaccionar al estado de otro slot, usáis URL params o context:
```tsx
// app/@notifications/page.tsx
import { obtenerNotificaciones } from '@/lib/api'
export default async function NotificationsPage({
searchParams,
}: {
searchParams: { filtro?: string }
}) {
const filtro = searchParams.filtro || 'todas'
const notificaciones = await obtenerNotificaciones(filtro)
return (
<div className="space-y-2">
{notificaciones.map((notif) => (
<div
key={notif.id}
className="p-3 border rounded hover:bg-gray-50"
>
<span className="font-medium">{notif.titulo}</span>
<span className="text-sm text-gray-500 ml-2">
{notif.fecha}
</span>
</div>
))}
</div>
)
}
```
Otro slot puede actualizar los searchParams para filtrar notificaciones. Cada slot reacciona de forma independiente a cambios en la URL.
El Patrón de 5 Capas para Resiliencia en Parallel Routes
Este framework os garantiza que cada slot paraleno tenga su propia estrategia de recuperación:
Capa 1: Diseño de slots independientes
Identificáis qué secciones de vuestra UI pueden cargarse de forma aislada. Cada slot debe poder fallar sin afectar a los demás.
Capa 2: Loading granular específico
Cada loading.tsx comunica exactamente qué está cargando. Skeleton que refleja la estructura real del contenido, no un placeholder genérico.
Capa 3: Error boundaries por slot
error.tsx con reset funcional. El usuario puede reintentar desde el slot específico sin refrescar la página.
Capa 4: Fallback con datos en caché
Implementáis stale-while-revalidate en cada slot. Si falla la API fresca, mostráis datos en caché con indicador de antigüedad.
Capa 5: Timeout y circuit breaker visual
Si un slot no responde en X segundos, mostráis estado de timeout. No dejáis al usuario esperando indefinidamente.
Implementación de timeout por slot:
```tsx
// app/@analytics/loading.tsx
import { TimeoutLoader } from '@/components/TimeoutLoader'
export default function AnalyticsLoading() {
return (
<TimeoutLoader timeoutMs={5000} fallback={<AnalyticsTimeout />}>
<AnalyticsSkeleton />
</TimeoutLoader>
)
}
function AnalyticsSkeleton() {
return (
<div className="animate-pulse space-y-4">
<div className="h-8 w-32 bg-gray-200 rounded" />
<div className="grid grid-cols-2 gap-4">
<div className="h-24 bg-gray-200 rounded" />
<div className="h-24 bg-gray-200 rounded" />
</div>
</div>
)
}
function AnalyticsTimeout() {
return (
<div className="p-4 border border-yellow-200 rounded-lg bg-yellow-50">
<p className="text-sm text-yellow-800">
Analytics tardando más de lo esperado
</p>
<button
onClick={() => window.location.reload()}
className="mt-2 text-sm text-yellow-600 hover:text-yellow-800 underline"
>
Recargar esta sección
</button>
</div>
)
}
```
Este timeout es independiente de los otros slots. @feed y @notifications siguen funcionando mientras @analytics muestra su estado de timeout.
Objections Resueltas
"Parallel Routes añaden complejidad innecesaria"
Cierto. Si vuestra app tiene una sola sección, no necesitáis Parallel Routes.
Pero si vuestra app tiene múltiples secciones que cargan datos independientes, Parallel Routes reducen complejidad a largo plazo. Cada slot es más simple de mantener que un sistema acoplado donde todo depende de todo.
"Los estados de carga independientes causan layout shifts"
El secreto: skeleton loaders que coinciden exactamente con las dimensiones del contenido final. Medís los componentes. Creáis skeletons con las mismas dimensiones exactas.
Además, Parallel Routes os permiten mostrar contenido en caché inmediatamente mientras свежие datos cargan. Menos layout shift, no más.
"Intercepting Routes son difíciles de debuggear"
Usáis logs estructurados en cada slot:
```tsx
// app/producto/@modal/default.tsx
import { trace } from '@vercel/analytics'
export default async function ProductoModal({ params }) {
const startTime = Date.now()
try {
const producto = await obtenerProducto(params.id)
trace('modal_renderizado', {
producto_id: params.id,
duracion_ms: Date.now() - startTime,
fuente: 'intercepted',
})
return <ProductoModalContent producto={producto} />
} catch (error) {
trace('modal_error', {
producto_id: params.id,
error: error.message,
})
throw error
}
}
```
Con logging por slot, sabéis exactamente qué falla y cuándo.
Key Takeaways
Parallel Routes transforman layouts complejos en sistemas resilientes.
Cada slot carga, falla, y se recupera de forma independiente.
Intercepting Routes permiten overlays y modales sin perder navegación contextual.
El Patrón de 5 Capas garantiza que ningún slot pueda colapsar toda la aplicación.
Implementáis loading.tsx y error.tsx específicos por slot. No más spinners globales.
No esperáis a que una API lenta bloquee toda la experiencia de usuario.
*El 90% de layouts fracasan por acoplamiento. Isolation is the feature, no the exception.*
Si vuestra aplicación tiene múltiples secciones con datos independientes, Parallel Routes no son opcionales. Son la diferencia entre una UI que falla con elegancia y una que arrastra al usuario a pantallas en blanco.
Empezad con un solo slot. Implementad loading.tsx específico. Agregad error.tsx con reset funcional. Escaláis cuando necesitéis. No antes.
Lee el artículo completo en brianmenagomez.com
Más sobre mis servicios en brianmenagomez.com
Herramientas: Conversor IAE CNAE · Gestorias cerca de ti · Calculadora IRPF

