Rutas Dinamicas, Loading y Errores
Rutas dinamicas
Usa corchetes en el nombre de la carpeta para crear rutas dinamicas:
app/
├── blog/
│ └── [slug]/
│ └── page.tsx → /blog/mi-articulo
├── productos/
│ └── [id]/
│ └── page.tsx → /productos/123
v16Cambio Cambio importante en v16: params ahora es una Promise. Debes usar await:
// app/blog/[slug]/page.tsx
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await getPost(slug)
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
)
}
Migracion desde v15
Si en v15 accedias a params.slug directamente (sin await), en v16 esto da error. Todos los params y searchParams deben llevar await.
searchParams tambien es async
// app/productos/page.tsx
export default async function ProductosPage({
searchParams,
}: {
searchParams: Promise<{ categoria?: string; orden?: string }>
}) {
const { categoria, orden } = await searchParams
const productos = await getProductos({ categoria, orden })
return (
<div>
<h1>Productos {categoria && `- ${categoria}`}</h1>
{productos.map((p) => (
<div key={p.id}>{p.nombre}</div>
))}
</div>
)
}
Catch-all routes
Para capturar multiples segmentos:
app/docs/[...slug]/page.tsx → /docs/a, /docs/a/b, /docs/a/b/c
export default async function DocsPage({
params,
}: {
params: Promise<{ slug: string[] }>
}) {
const { slug } = await params
// slug = ["a", "b", "c"] para /docs/a/b/c
return <div>Pagina: {slug.join("/")}</div>
}
generateStaticParams
Para generar rutas estaticas en build time (SSG):
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getAllPosts()
return posts.map((post) => ({
slug: post.slug,
}))
}
Esto genera las paginas en build time. Mas rapido y mejor para SEO.
Loading UI
Crea un loading.tsx en cualquier carpeta y Next.js lo muestra automaticamente mientras la pagina carga:
// app/blog/loading.tsx
export default function Loading() {
return (
<div className="animate-pulse space-y-4">
<div className="h-8 bg-gray-700 rounded w-3/4" />
<div className="h-4 bg-gray-700 rounded w-full" />
<div className="h-4 bg-gray-700 rounded w-5/6" />
<div className="h-4 bg-gray-700 rounded w-4/6" />
</div>
)
}
Internamente, Next.js envuelve la pagina en un Suspense boundary. El loading se muestra mientras el Server Component resuelve sus await.
Loading granular
Para control mas fino, usa Suspense directamente:
import { Suspense } from "react"
export default function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<div>Cargando ventas...</div>}>
<VentasChart />
</Suspense>
<Suspense fallback={<div>Cargando pedidos...</div>}>
<PedidosRecientes />
</Suspense>
</div>
)
}
Cada seccion carga de forma independiente. Si VentasChart tarda 3 segundos y PedidosRecientes tarda 1 segundo, el usuario ve los pedidos primero.
Error handling
Crea un error.tsx para capturar errores en una ruta:
"use client" // error.tsx DEBE ser Client Component
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div className="text-center py-12">
<h2 className="text-2xl font-bold mb-4">Algo salio mal</h2>
<p className="text-gray-400 mb-6">{error.message}</p>
<button
onClick={() => reset()}
className="bg-blue-600 text-white px-4 py-2 rounded"
>
Intentar de nuevo
</button>
</div>
)
}
error.tsxcaptura errores de su ruta y todas las rutas hijasreset()intenta re-renderizar el componente que fallo- El error no se propaga a los layouts padres
not-found
Para mostrar una UI 404 personalizada:
// app/blog/[slug]/not-found.tsx
export default function NotFound() {
return (
<div className="text-center py-12">
<h2 className="text-2xl font-bold">Articulo no encontrado</h2>
<p className="text-gray-400">El articulo que buscas no existe.</p>
</div>
)
}
Lo activas desde tu page con notFound():
import { notFound } from "next/navigation"
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await getPost(slug)
if (!post) {
notFound()
}
return <article>{/* ... */}</article>
}
Ejemplo completo: blog con todo
app/blog/
├── page.tsx # Lista de posts
├── loading.tsx # Skeleton mientras carga la lista
├── error.tsx # Error al cargar la lista
└── [slug]/
├── page.tsx # Post individual
├── loading.tsx # Skeleton del post
├── error.tsx # Error al cargar el post
└── not-found.tsx # Post no encontrado
Cada nivel tiene su propio loading, error y not-found. Si el post individual falla, la lista sigue funcionando.