tutoriales.com

¡Rutas Dinámicas y Anidadas en Next.js con el App Router! 🚀 Guía Completa

Este tutorial te guiará a través del potente sistema de enrutamiento dinámico y anidado de Next.js, utilizando el App Router. Aprenderás a crear rutas flexibles, pasar parámetros de URL y estructurar tus aplicaciones con layouts compartidos, mejorando la escalabilidad y la organización de tu código.

Intermedio20 min de lectura8 views
Reportar error

Next.js, con su App Router, ha revolucionado la forma en que construimos aplicaciones web modernas y escalables. Una de las características más poderosas y esenciales para cualquier aplicación del mundo real es la capacidad de manejar rutas dinámicas y anidadas. Esto te permite crear URLs flexibles que responden a datos específicos (como el ID de un producto o el nombre de un usuario) y organizar tu interfaz de usuario con layouts compartidos que persisten a través de diferentes segmentos de la URL.

En esta guía completa, exploraremos a fondo cómo implementar y aprovechar al máximo las rutas dinámicas y anidadas utilizando el App Router de Next.js. Desde la configuración básica hasta patrones avanzados, te proporcionaremos los conocimientos y ejemplos prácticos necesarios para construir aplicaciones robustas y eficientes.


¿Qué es el App Router en Next.js? 🤔

Antes de sumergirnos en las rutas dinámicas, es fundamental entender el contexto del App Router. Introducido en Next.js 13 y estable en Next.js 14, el App Router es un nuevo modelo de enrutamiento construido sobre React Server Components y diseñado para la flexibilidad y el rendimiento. A diferencia del antiguo pages directory, el app directory permite un control más granular sobre el renderizado (cliente vs. servidor) y facilita la creación de layouts complejos.

💡 Consejo: Si vienes del `pages` directory, el App Router introduce un cambio de paradigma significativo. Te recomendamos familiarizarte con los Server Components y Client Components antes de empezar.

Ventajas clave del App Router:

  • React Server Components: Renderizado en el servidor por defecto, lo que mejora el rendimiento inicial y el SEO.
  • Layouts anidados: Facilita la creación de interfaces de usuario complejas con componentes compartidos entre rutas.
  • Manejo de errores mejorado: error.js y not-found.js para una mejor experiencia de usuario.
  • Streaming y carga de datos: Soporte nativo para la carga de datos asíncrona.
  • Patrones de carga UI: Uso de loading.js para indicar estados de carga.

Rutas Dinámicas: Creando URLs Flexibles ✨

Las rutas dinámicas son la columna vertebral de cualquier aplicación que necesite mostrar contenido basado en datos variables. Imagina una tienda online: cada producto tiene su propia página, pero no quieres crear un archivo HTML o un componente para cada uno. Ahí es donde entran las rutas dinámicas.

En el App Router, las rutas dinámicas se definen utilizando corchetes [] en el nombre de la carpeta.

1. Conceptos Básicos de Rutas Dinámicas

Para crear una ruta dinámica, simplemente nombra una carpeta con corchetes. El contenido dentro de los corchetes será el parámetro de la ruta.

Por ejemplo, para una página de perfil de usuario, tendrías una estructura como esta:

// app/users/[userId]/page.js
export default function UserProfilePage({ params }) {
  return (
    <div>
      <h1>Perfil del Usuario: {params.userId}</h1>
      {/* ... más contenido del perfil ... */}
    </div>
  );
}

En este ejemplo, [userId] es el segmento dinámico. Si navegas a /users/123, params.userId será "123".

Accediendo a los parámetros dinámicos

El componente page.js o layout.js recibe un prop params que es un objeto con los segmentos dinámicos de la URL como propiedades. Si tienes múltiples segmentos dinámicos, todos aparecerán en este objeto.

📌 Nota: Los parámetros de ruta siempre son `strings`. Si necesitas usarlos como números, recuerda convertirlos con `parseInt()` o `Number()`.

2. Ejemplos Prácticos de Rutas Dinámicas

Vamos a construir un ejemplo sencillo para una aplicación de blog con posts dinámicos.

Estructura de archivos:

app/
├── blog/
│   └── [slug]/
│       └── page.js
└── page.js

Código para app/blog/[slug]/page.js:

// app/blog/[slug]/page.js

import { notFound } from 'next/navigation';

// Simulamos una base de datos de posts
const posts = [
  { slug: 'mi-primer-post', title: 'Mi Primer Post en Next.js', content: 'Contenido del primer post...' },
  { slug: 'tutorial-nextjs', title: 'Tutorial de Next.js Avanzado', content: 'Aquí aprenderás Next.js a fondo...' },
];

export default function BlogPostPage({ params }) {
  const { slug } = params;
  const post = posts.find(p => p.slug === slug);

  if (!post) {
    notFound(); // Muestra la página 404 de Next.js si el post no existe
  }

  return (
    <main>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
      <p>Este post se generó dinámicamente con el slug: <mark>{slug}</mark></p>
    </main>
  );
}

// Función para generar rutas estáticas durante el build (para SSG)
export async function generateStaticParams() {
  return posts.map((post) => ({
    slug: post.slug,
  }));
}
¿Qué es `generateStaticParams()`? 🤔 `generateStaticParams()` es una función especial que Next.js ejecuta en el servidor durante el proceso de *build*. Permite pre-renderizar rutas dinámicas con antelación, lo que es ideal para contenido que no cambia con frecuencia. Para cada objeto devuelto por `generateStaticParams()`, Next.js generará una página estática para esa combinación de parámetros. Esto es esencial para la Generación Estática de Sitios (SSG) con rutas dinámicas. Si no se usa, la ruta se considerará Dinámica Server-Side (SSR) o Server-Side Rendering en cada solicitud.

3. Rutas Dinámicas Catch-all [...slug] 🎣

A veces, necesitas una ruta dinámica que capture todos los segmentos restantes de la URL. Para esto, usa el patrón catch-all con [...slug]. Esto es útil para rutas como /docs/a/b/c donde a/b/c podría ser una estructura de subcarpetas.

Estructura de archivos:

app/
├── docs/
│   └── [...slug]/
│       └── page.js
└── page.js

Código para app/docs/[...slug]/page.js:

// app/docs/[...slug]/page.js
export default function DocsPage({ params }) {
  // params.slug será un array de strings, e.g., ['a', 'b', 'c']
  const docPath = params.slug ? params.slug.join('/') : 'Inicio';

  return (
    <main>
      <h1>Documentación para: {docPath}</h1>
      <p>Esta página captura múltiples segmentos de URL.</p>
      <ul>
        {params.slug.map((segment, index) => (
          <li key={index}>Segmento {index + 1}: <strong>{segment}</strong></li>
        ))}
      </ul>
    </main>
  );
}

export async function generateStaticParams() {
  return [
    { slug: ['getting-started'] },
    { slug: ['components', 'buttons'] },
    { slug: ['components', 'forms', 'input'] },
  ];
}

Si accedes a /docs/components/buttons, params.slug será ['components', 'buttons'].

⚠️ Advertencia: Un segmento catch-all `[...slug]` capturará rutas como `/docs/a` o `/docs/a/b`. Para hacer el segmento opcional (es decir, que también capture `/docs`), usa `[[...slug]]` (dobles corchetes).

Rutas Anidadas y Layouts Compartidos 📂

Las rutas anidadas son la forma en que el App Router organiza y renderiza segmentos de UI basados en la estructura de carpetas. Cuando tienes rutas anidadas, puedes definir layouts que envuelvan el contenido de las rutas hijas, permitiendo que una parte de tu UI permanezca constante mientras el contenido principal cambia.

1. Creando Layouts Anidados

Un layout.js es un componente React que envuelve un page.js o cualquier layout.js hijo. Recibe un prop children que contiene el contenido de la ruta anidada.

Estructura de archivos:

app/
├── dashboard/
│   ├── analytics/
│   │   └── page.js
│   ├── settings/
│   │   └── profile/
│   │       └── page.js
│   │   └── page.js
│   └── layout.js  <-- Layout para /dashboard y sus subrutas
├── page.js
└── layout.js      <-- Layout Root para toda la aplicación

Código para app/layout.js (Layout Root):

// app/layout.js
import './globals.css'; // Estilos globales

export const metadata = {
  title: 'Mi App Next.js con Rutas Anidadas',
  description: 'Explorando layouts y rutas dinámicas.',
};

export default function RootLayout({ children }) {
  return (
    <html lang="es">
      <body>
        <header style={{ background: '#eee', padding: '10px' }}>
          <h1>Cabecera Global</h1>
          <nav>
            <a href="/">Inicio</a> | <a href="/dashboard">Dashboard</a> | <a href="/blog/mi-primer-post">Blog Post</a>
          </nav>
        </header>
        {children}
        <footer style={{ background: '#eee', padding: '10px', marginTop: '20px' }}>
          <p>© 2024 Mi Empresa</p>
        </footer>
      </body>
    </html>
  );
}

Código para app/dashboard/layout.js (Layout para Dashboard):

// app/dashboard/layout.js

export const metadata = {
  title: 'Dashboard de Usuario',
};

export default function DashboardLayout({ children }) {
  return (
    <section style={{ border: '1px solid #ccc', padding: '20px', margin: '20px' }}>
      <h2>Panel de Control</h2>
      <nav>
        <a href="/dashboard">Inicio Dashboard</a> | <a href="/dashboard/settings">Ajustes</a> | <a href="/dashboard/analytics">Analíticas</a>
      </nav>
      <div style={{ marginTop: '15px' }}>
        {children} {/* Aquí se renderiza el contenido de las rutas hijas */}
      </div>
    </section>
  );
}

Código para app/dashboard/page.js:

// app/dashboard/page.js
export default function DashboardIndexPage() {
  return (
    <div>
      <h3>Bienvenido al Dashboard</h3>
      <p>Selecciona una opción del menú lateral.</p>
    </div>
  );
}

Código para app/dashboard/settings/page.js:

// app/dashboard/settings/page.js
export default function SettingsPage() {
  return (
    <div>
      <h3>Ajustes Generales</h3>
      <p>Configura tus preferencias aquí.</p>
    </div>
  );
}

Cuando accedas a /dashboard/settings, verás:

  • La cabecera y el pie de página de app/layout.js.
  • El Panel de Control y la navegación del app/dashboard/layout.js.
  • El contenido de app/dashboard/settings/page.js dentro del <div style={{ marginTop: '15px' }}> del layout del dashboard.
RootLayout app/layout.js (HTML, Body, Navbar Global) DashboardLayout app/dashboard/layout.js (Sidebar, Dashboard Header) {children} DashboardIndex app/dashboard/ page.js Contenido General SettingsPage app/dashboard/ settings/page.js Ajustes Perfil AnalyticsPage app/dashboard/ analytics/page.js Gráficas Datos Layouts Anidados

2. Layouts Anidados con Rutas Dinámicas

Podemos combinar la potencia de las rutas dinámicas con los layouts anidados. Por ejemplo, un layout que se aplique a todos los perfiles de usuario, pero el contenido de cada perfil sea dinámico.

Estructura de archivos:

app/
├── users/
│   └── [userId]/
│       ├── layout.js <-- Layout específico para un usuario
│       ├── page.js
│       └── settings/
│           └── page.js
└── page.js

Código para app/users/[userId]/layout.js:

// app/users/[userId]/layout.js

import { notFound } from 'next/navigation';

// Simulamos usuarios
const users = [
  { id: '1', name: 'Alice' },
  { id: '2', name: 'Bob' },
];

export default function UserLayout({ children, params }) {
  const { userId } = params;
  const user = users.find(u => u.id === userId);

  if (!user) {
    notFound();
  }

  return (
    <div style={{ border: '2px dashed blue', padding: '15px', margin: '15px' }}>
      <h2>Usuario: <span class="badge yellow">{user.name} (ID: {userId})</span></h2>
      <nav>
        <a href={`/users/${userId}`}>Ver Perfil</a> | 
        <a href={`/users/${userId}/settings`}>Ajustes del Usuario</a>
      </nav>
      <hr />
      {children}
    </div>
  );
}

export async function generateStaticParams() {
  return users.map((user) => ({
    userId: user.id,
  }));
}

Código para app/users/[userId]/page.js:

// app/users/[userId]/page.js
export default function UserProfilePage({ params }) {
  return (
    <div>
      <h3>Información General del Perfil</h3>
      <p>Aquí se mostrarían los detalles del usuario {params.userId}.</p>
    </div>
  );
}

Código para app/users/[userId]/settings/page.js:

// app/users/[userId]/settings/page.js
export default function UserSettingsPage({ params }) {
  return (
    <div>
      <h3>Ajustes Específicos del Usuario</h3>
      <p>Modifica la configuración de {params.userId} aquí.</p>
    </div>
  );
}

Ahora, si navegas a /users/1, verás el UserLayout con el nombre de Alice, y dentro, el contenido de UserProfilePage. Si navegas a /users/1/settings, el UserLayout persistirá, pero el contenido interno cambiará a UserSettingsPage.

3. Slots para UI Paralela (Advanced) 🧩

El App Router también permite la UI paralela mediante slots con @folderName. Esto es útil para renderizar una o más rutas independientes en la misma vista, por ejemplo, dashboards con múltiples paneles que pueden cargar su propio estado de forma asíncrona.

Aunque es un tema más avanzado, es bueno saber que existe. Los slots se definen con @folderName y se pasan como props al layout.js que los contiene.

app/
├── dashboard/
│   ├── @team/
│   │   └── page.js
│   ├── @analytics/
│   │   └── page.js
│   ├── layout.js
│   └── page.js

En app/dashboard/layout.js recibirías @team y @analytics como props, además de children:

export default function DashboardLayout({ children, team, analytics }) {
  return (
    <section>
      {children}
      {team} {/* Renderiza el slot del equipo */}
      {analytics} {/* Renderiza el slot de analíticas */}
    </section>
  );
}
🔥 Importante: Los slots son una característica avanzada y están fuera del alcance de un tutorial introductorio completo. Se mencionan aquí para que sepas de su existencia y dónde encajan en el sistema de enrutamiento.

Navegación y Enlaces 🔗

Para navegar entre rutas dinámicas y anidadas, Next.js proporciona el componente Link de next/link y el hook useRouter de next/navigation.

1. Usando <Link>

next/link es la forma recomendada para la navegación del lado del cliente. Permite pre-fetch de las rutas en segundo plano para una navegación instantánea.

import Link from 'next/link';

export default function HomePage() {
  const userId = '3';
  const postId = 'tutorial-nextjs';

  return (
    <div>
      <h1>Inicio</h1>
      <nav>
        <ul>
          <li><Link href="/dashboard">Ir al Dashboard</Link></li>
          <li><Link href={`/users/${userId}`}>Ver perfil de usuario {userId}</Link></li>
          <li><Link href={`/blog/${postId}`}>Leer post: {postId}</Link></li>
          <li><Link href="/docs/getting-started">Documentación</Link></li>
        </ul>
      </nav>
    </div>
  );
}

2. Usando useRouter (para navegación programática) 🧑‍💻

Para la navegación programática (por ejemplo, después de enviar un formulario o una acción de usuario), usa el hook useRouter.

⚠️ Advertencia: `useRouter` es un Client Component Hook. Si necesitas navegar desde un Server Component, pasa la URL a un Client Component o usa un `form` HTML para POST requests.
// app/dashboard/settings/page.js (Ejemplo dentro de un Client Component)
'use client'; // Indica que es un Client Component

import { useRouter } from 'next/navigation';

export default function UserSettingsPage() {
  const router = useRouter();

  const handleSaveSettings = () => {
    // Lógica para guardar ajustes...
    console.log('Ajustes guardados!');
    // Redirigir al perfil del usuario después de guardar
    router.push('/dashboard'); 
  };

  return (
    <div>
      <h3>Ajustes Específicos del Usuario</h3>
      <p>Modifica la configuración aquí.</p>
      <button onClick={handleSaveSettings}>Guardar Ajustes y Volver</button>
    </div>
  );
}
90% Completado

Casos de Uso Avanzados y Buenas Prácticas 🎯

1. Metadata Dinámica

Con el App Router, la metadata (<title>, <meta description>, etc.) se puede definir en cualquier layout.js o page.js. Para rutas dinámicas, puedes generar metadata dinámica basada en los parámetros de la ruta.

// app/blog/[slug]/page.js

import { notFound } from 'next/navigation';

const posts = [
  { slug: 'mi-primer-post', title: 'Mi Primer Post en Next.js', description: 'Todo sobre mi primer post.' },
  { slug: 'tutorial-nextjs', title: 'Tutorial de Next.js Avanzado', description: 'Guía completa de Next.js.' },
];

export async function generateMetadata({ params }) {
  const post = posts.find(p => p.slug === params.slug);
  if (!post) {
    return { title: 'Post No Encontrado' };
  }
  return {
    title: post.title,
    description: post.description,
  };
}

export default function BlogPostPage({ params }) {
  const post = posts.find(p => p.slug === params.slug);
  if (!post) {
    notFound();
  }
  return (
    <main>
      <h1>{post.title}</h1>
      <p>{post.description}</p>
    </main>
  );
}

2. Manejo de Errores con error.js y not-found.js

El App Router facilita el manejo de errores específicos por segmento. Puedes definir archivos error.js y not-found.js en cualquier nivel de la jerarquía de rutas. error.js captura errores de renderizado en tiempo de ejecución, mientras que not-found.js se muestra cuando se llama a notFound() o una ruta no existe.

app/
├── blog/
│   ├── [slug]/
│   │   ├── error.js  <-- Maneja errores para posts específicos
│   │   └── page.js
│   ├── not-found.js  <-- Maneja 404 para la ruta /blog/*
│   └── page.js
├── error.js        <-- Maneja errores globales
├── not-found.js    <-- Maneja 404 globales
└── page.js
Ejemplo de `error.js` ```javascript 'use client'; // Componentes de Error deben ser Client Components

export default function Error({ error, reset }) { return (

Algo salió mal!

{error.message}

<button onClick={() => reset()}>Intentar de nuevo
); }

</details>

<details open><summary>Ejemplo de `not-found.js`</summary>
```javascript
import Link from 'next/link';

export default function NotFound() {
  return (
    <div>
      <h2>No Encontrado</h2>
      <p>La página que buscas no existe.</p>
      <Link href="/">Volver al inicio</Link>
    </div>
  );
}

Comparativa: App Router vs. Pages Router (Rutas Dinámicas/Anidadas) 🆚

Es útil ver las diferencias clave si vienes del Pages Router.

CaracterísticaPages Router (pages/)App Router (app/)
Rutas Dinámicaspages/posts/[id].jsapp/posts/[id]/page.js
Rutas Catch-allpages/docs/[...slug].jsapp/docs/[...slug]/page.js
Layouts AnidadosRequiere archivos _app.js y lógica manual, _document.js para HTML.layout.js en cada nivel de carpeta, root layout.js para HTML.
Acceso a parámetrosuseRouter().query (Client-side) en getStaticProps/getServerSideProps (Server-side)params prop en page.js/layout.js (Server/Client), useSearchParams() (Client)
RenderizadoPrincipalmente Client-side (CSR) con opciones SSR/SSG a nivel de página.Predominantemente Server-side (RSC) por defecto.
MetadataCon next/head dentro de componentes de página.Con metadata objeto/función en layout.js/page.js.
Manejo de Errorespages/404.js, pages/500.jsnot-found.js, error.js en cualquier nivel de ruta.
Tutorial Completado

Conclusión 🎉

El sistema de rutas dinámicas y anidadas del App Router de Next.js es una herramienta extremadamente potente que te permite construir aplicaciones web complejas y bien organizadas con facilidad. Al dominar los conceptos de [param] para rutas dinámicas, [...param] para rutas catch-all y el uso estratégico de layout.js para layouts compartidos, estarás bien equipado para crear experiencias de usuario excepcionales y mantener tu código base limpio y escalable.

Recuerda practicar estos conceptos construyendo tus propias aplicaciones. Experimenta con la combinación de rutas dinámicas y anidadas, y no dudes en consultar la documentación oficial de Next.js para profundizar aún más. ¡Feliz codificación!

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!