Rutas Dinamicas, Loading y Errores

Rutas dinamicas

Usa corchetes en el nombre de la carpeta para crear rutas dinamicas:

code
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:

tsx
// 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

tsx
// 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:

code
app/docs/[...slug]/page.tsx    → /docs/a, /docs/a/b, /docs/a/b/c
tsx
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):

tsx
// 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:

tsx
// 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:

tsx
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:

tsx
"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.tsx captura errores de su ruta y todas las rutas hijas
  • reset() intenta re-renderizar el componente que fallo
  • El error no se propaga a los layouts padres

not-found

Para mostrar una UI 404 personalizada:

tsx
// 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():

tsx
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

code
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.