Layouts anidados en Next.js App Router: cómo el root layout envuelve todo

DUGLAS MORENODUGLAS MORENO 👁 7

Introducción

Si estás empezando a trabajar con Next.js 13+ y su nuevo App Router, seguramente te encontraste con la idea de que los layouts pueden anidarse y que el layout raíz (root layout) envuelve a toda la aplicación. Este concepto es clave para lograr una UI consistente, evitar duplicar código y controlar exactamente qué partes de la página se vuelven a renderizar al navegar entre rutas. En este artículo vas a aprender, con ejemplos prácticos y su salida esperada, cómo funciona el árbol de layouts en el App Router, cómo crear layouts anidados para secciones específicas de tu sitio y cuándo usar route groups para excluir ciertas rutas del envoltorio del root layout.

Este post está dirigido a desarrolladores que ya conocen los fundamentos de React y Next.js, pero quieren profundizar en el manejo de layouts en el App Router. Al finalizar, sabrás estructurar layouts de forma eficiente, evitar errores comunes y aplicar buenas prácticas que mejoran el rendimiento y la mantenibilidad de tus proyectos.

¿Qué es el App Router?

El App Router es el nuevo sistema de enrutamiento introducido en Next.js 13. Se basa en el concepto de server components por defecto y utiliza la carpeta app para definir rutas, layouts y páginas. Cada segmento de ruta dentro de app puede contener un archivo layout.tsx (o .tsx) que envuelve a las páginas y a los layouts de los segmentos hijos. El archivo app/layout.tsx es el layout raíz y se aplica a todas las rutas de la aplicación.

A diferencia del antiguo Pages Router, donde el componente _app.js era el único punto de envoltorio global, el App Router permite un envoltorio más granular: puedes tener un layout que solo afecte a un segmento de ruta y sus hijos, mientras que otros segmentos usan sus propios layouts o ninguno.

El root layout

El archivo app/layout.tsx es obligatorio. Next.js lo usa como envoltorio de todo el árbol de rutas. Dentro de este archivo debes incluir los elementos <html> y <body> (aunque Next.js los agrega automáticamente si los omites, es buena práctica definirlos explícitamente para controlar metadata y estilos globales). Además, el componente debe recibir una prop children que representa el contenido de la ruta actual.

Ejemplo básico de root layout:

// app/layout.tsx
export const metadata = {
  title: 'Mi App Next.js',
  description: 'Descripción de la aplicación',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang='es'>
      <head>
        {/* metadata se inyecta automáticamente */}
      </head>
      <body>
        <header>
          <h1>Mi Sitio</h1>
          <nav>
            <a href='/'>Inicio</a> | {
              ' '
            }
            <a href='/dashboard'>Dashboard</a>
          </nav>
        </header>
        <main>{children}</main>
        <footer>
          <p>&copy; {new Date().getFullYear()} Mi Empresa</p>
        </footer>
      </body>
    </html>
  )
}

Este layout agrega un encabezado, un menú de navegación y un pie de página que estarán presentes en todas las páginas de la aplicación, sin importar cuán profundo sea el segmento de ruta.

Layouts anidados: concepto básico

Cuando creas un archivo layout.tsx dentro de un segmento de ruta (por ejemplo, app/dashboard/layout.tsx), ese layout solo envuelve a las páginas y layouts que se encuentren dentro de ese mismo segmento o sus subsegmentos. El comportamiento es jerárquico: el root layout se renderiza primero, luego el layout del segmento padre, luego el del segmento hijo, y finalmente la página.

Puedes pensar en esto como capas de una cebolla: cada layout agrega su propio markup alrededor de children. El orden de renderizado es de afuera hacia adentro: root → layout de segmento → layout de subsegmento → página.

Esta característica permite:

  • Mantener una estructura de layout global (header, footer, estilos globales) en el root layout.
  • Añadir capas específicas para secciones grandes (por ejemplo, un sidebar para el dashboard).
  • Evitar volver a enviar código innecesario al cliente cuando solo cambia una sección interna.

Ejemplo 1: Layout raíz + layout de dashboard

Vamos a construir una aplicación donde el root layout provea un header y un footer comunes, y el layout del segmento dashboard agregue una barra lateral izquierda. Dentro del dashboard tendremos dos páginas: una vista general y una página de configuración.

Estructura de archivos

app/
  layout.tsx          # root layout
  dashboard/
    layout.tsx        # layout de dashboard (sidebar)
    page.tsx          # vista general del dashboard
    settings/
      layout.tsx      # layout opcional para settings (podemos dejarlo vacío)
      page.tsx        # página de configuración

app/layout.tsx (root layout)

// app/layout.tsx
export const metadata = {
  title: 'Mi App Next.js',
  description: 'Aplicación con layouts anidados',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang='es'>
      <head>
        {/* Next.js inyecta metadata automáticamente */}
      </head>
      <body>
        <header style={{ background: '#003366', color: '#fff', padding: '1rem' }}>
          <h1 style={{ margin: 0 }}>Mi App Next.js</h1>
        </header>
        <main style={{ display: 'flex', minHeight: 'calc(100vh - 80px)' }}>
          {children}
        </main>
        <footer style={{ background: '#003366', color: '#fff', textAlign: 'center', padding: '1rem' }}>
          <p>&copy; {new Date().getFullYear()} Mi Empresa</p>
        </footer>
      </body>
    </html>
  )
}

app/dashboard/layout.tsx (layout de dashboard)

// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div style={{ display: 'flex', width: '100%' }}>
      <aside style={{ background: '#f0f0f0', padding: '1rem', width: '200px', borderRight: '1px solid #ccc' }}>
        <nav>
          <ul style={{ listStyle: 'none', padding: 0 }}>
            <li>
              <a href='/dashboard' style={{ textDecoration: 'none', color: '#0066cc' }}>
                Vista general
              </a>
            </li>
            <li>
              <a href='/dashboard/settings' style={{ textDecoration: 'none', color: '#0066cc' }}>
                Configuración
              </a>
            </li>
          </ul>
        </nav>
      </aside>
      <section style={{ flex: 1, padding: '2rem' }}>{children}</section>
    </div>
  )
}

app/dashboard/page.tsx (vista general)

// app/dashboard/page.tsx
export default function DashboardPage() {
  return (
    <div>
      <h2>Vista general del dashboard</h2>
      <p>Bienvenido al panel de control. Aquí puedes ver resúmenes, métricas y accesos rápidos.</p>
    </div>
  )
}

app/dashboard/settings/page.tsx (página de configuración)

// app/dashboard/settings/page.tsx
export default function SettingsPage() {
  return (
    <div>
      <h2>Configuración</h2>
      <p>Administra tu cuenta, notificaciones y preferencias.</p>
    </div>
  )
}

Salida esperada

Si navegas a http://localhost:3000/dashboard, el HTML resultante tendrá aproximadamente esta estructura (los estilos están omitidos para claridad):

<html lang="es">
  <head>...</head>
  <body>
    <header>Mi App Next.js</header>
    <main>
      <div style="display:flex;width:100%">
        <aside>
          <nav>
            <ul>
              <li><a href=\"\/dashboard\">Vista general</a></li>
              <li><a href=\"\/dashboard\/settings\">Configuración</a></li>
            </ul>
          </nav>
        </aside>
        <section>
          <h2>Vista general del dashboard</h2>
          <p>Bienvenido al panel de control....</p>
        </section>
      </div>
    </main>
    <footer>&copy; 2025 Mi Empresa</footer>
  </body>
</html>

Al ir a /dashboard/settings, el <section> contendrá el contenido de SettingsPage, pero el header, footer y la sidebar permanecen iguales porque están definidos en los layouts que envuelven esa ruta.

Ejemplo 2: Layout de autenticación con route groups

Supongamos que quieres que las rutas de autenticación (login, register) tengan un layout distinto, sin el header ni el footer del sitio principal, pero sin perder los beneficios de los server components y el streaming. Para lograrlo, usas route groups: una carpeta cuyo nombre está entre paréntesis, como (auth), que le indica a Next.js que ese segmento no participe en la URL pública, pero sí puede contener layouts y páginas.

Evitar que el root layout envuelva ciertas rutas

El root layout sigue aplicándose a todas las rutas bajo app. Si quieres que ciertas rutas no reciban el header y footer del root layout, tienes dos opciones:

  1. Sobrescribir el <html> y <body> dentro de un layout más específico (no recomendado porque rompe el contrato de metadata).
  2. Crear un layout que envuelva solo el grupo de rutas y, dentro del root layout, renderizar children sin agregar header/footer para esas rutas mediante una comprobación de la ruta actual. Esta segunda opción es más compleja y suele evitarse.

La forma más sencilla y recomendada es no colocar el header y footer en el root layout, sino trasladarlos a un layout que se aplique solo a las rutas que los necesiten. Sin embargo, en muchos proyectos el root layout sí contiene header y footer globales, y se quiere excluirlos para unas pocas rutas. En ese caso, se puede usar un layout de grupo que sobrescriba el comportamiento del root layout mediante la técnica de layout groups: definir un layout en el grupo que retorne solo children sin agregar nada, y luego volver a envolver esas rutas con un layout específico. Pero la forma más limpia es reorganizar: mover el header y footer a un layout que se aplique a la mayor parte de la app y dejar el root layout solo para metadata y etiquetas <html>/<body>.

Vamos a mostrar la estrategia recomendada: el root layout solo provee metadata y las etiquetas básicas; un layout llamado app/layout.tsx (renombrado a app/base-layout.tsx para no confundir) se encarga de header/footer; y el grupo (auth) tiene su propio layout sin esos componentes.

Estructura de archivos con route groups

app/
  layout.tsx          # root layout (solo metadata y html/body)
  base-layout.tsx     # layout global con header y footer (opcional)
  (auth)/
    layout.tsx        # layout de autenticación (sin header/footer)
    login/page.tsx
    register/page.tsx
  dashboard/
    layout.tsx        # layout de dashboard (usa base-layout como wrapper)
    page.tsx

app/layout.tsx (root layout mínimo)

// app/layout.tsx
export const metadata = {
  title: 'Mi App Next.js',
  description: 'App con route groups',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang='es'>
      <head>
        {/* metadata se inyecta */}
      </head>
      <body>
        {children}
      </body>
    </html>
  )
}

app/base-layout.tsx (layout global opcional)

// app/base-layout.tsx
export default function BaseLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <>
      <header style={{ background: '#003366', color: '#fff', padding: '1rem' }}>
        <h1 style={{ margin: 0 }}>Mi App Next.js</h1>
      </header>
      <main style={{ padding: '2rem' }}>{children}</main>
      <footer style={{ background: '#003366', color: '#fff', textAlign: 'center', padding: '1rem' }}>
        <p>&copy; {new Date().getFullYear()} Mi Empresa</p>
      </footer>
    </>
  )
}

app/(auth)/layout.tsx (layout de autenticación)

// app/(auth)/layout.tsx
export default function AuthLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div style={{ maxWidth: '400px', margin: '2rem auto', padding: '2rem', border: '1px solid #ddd', borderRadius: '8px' }}>
      {children}
    </div>
  )
}

app/(auth)/login/page.tsx

// app/(auth)/login/page.tsx
export default function LoginPage() {
  return (
    <form>
      <h2>Iniciar sesión</h2>
      <label>
        Usuario:
        <input type='text' required style={{ display: 'block', margin: '0.5rem 0 1rem 0', width: '100%' }} />
      </label>
      <label>
        Contraseña:
        <input type='password' required style={{ display: 'block', margin: '0.5rem 0 1rem 0', width: '100%' }} />
      </label>
      <button type='submit' style={{ background: '#0066cc', color: '#fff', padding: '0.5rem 1rem', border: 'none', cursor: 'pointer' }}>
        Entrar
      </button>
    </form>
  )
}

app/(auth)/register/page.tsx (similar)

// app/(auth)/register/page.tsx
export default function RegisterPage() {
  return (
    <form>
      <h2>Crear cuenta</h2>
      <label>
        Email:
        <input type='email' required style={{ display: 'block', margin: '0.5rem 0 1rem 0', width: '100%' }} />
      </label>
      <label>
        Password:
        <input type='password' required style={{ display: 'block', margin: '0.5rem 0 1rem 0', width: '100%' }} />
      </label>
      <button type='submit' style={{ background: '#28a745', color: '#fff', padding: '0.5rem 1rem', border: 'none', cursor: 'pointer' }}>
        Registrarse
      </button>
    </form>
  )
}

Salida esperada para /login

El HTML que se enviará al cliente será algo como:

<html lang="es">
  <head>...</head>
  <body>
    <div style="max-width:400px;margin:2rem auto;padding:2rem;border:1px solid #ddd;border-radius:8px">
      <form>
        <h2>Iniciar sesión</h2>
        <!-- inputs y botón -->
      </form>
    </div>
  </body>
</html>

Observa que no hay header ni footer del sitio principal; solo el contenedor centrado del layout de autenticación. Esto se logra porque el segmento (auth) está excluido de la URL pública y su layout no importa el base-layout.

Si en cambio visitas /dashboard, el árbol de layouts será:

  1. RootLayout (solo html/body)
  2. BaseLayout (header, main, footer)
  3. DashboardLayout (sidebar + sección)
  4. DashboardPage o SettingsPage

De esta forma, el header y footer aparecen solo en las rutas que importan BaseLayout.

Buenas prácticas

  • Mantén los layouts ligeros: evita colocar lógica pesada o suscripciones a eventos dentro de un layout, ya que se renderizan en cada navegación que pase por ese segmento. Si necesitas datos que cambian frecuentemente, usa server components en la página o client components con useEffect y swr o react-query.
  • Aprovecha el streaming: los layouts son server components por defecto, lo que permite que Next.js envíe el HTML del layout mientras sigue obteniendo los datos de la página. No bloquees el layout con operaciones síncronas lentas.
  • Usa children correctamente: siempre recibe la prop children y la renderizas exactamente una vez. No la ignores ni la dupliques, porque podría provocar que el contenido se renderice de forma inesperada o se pierda.
  • Evita la duplicación de metadata: define metadata en el root layout o en layouts específicos usando el objeto export const metadata. No intentes modificar las etiquetas <meta> manualmente dentro del JSX, ya que Next.js las gestiona automáticamente.
  • Agrupa rutas con sentido: los route groups (nombre) son útiles para crear secciones que compartan layout pero no quieras que aparezcan en la URL (como (auth), (marketing), (docs)). Mantén los nombres cortos y descriptivos.
  • Tipado de children: tipa la prop como React.ReactNode para aceptar cualquier tipo de nodo React (elementos, fragmentos, strings).
  • Estilos: si usas CSS Modules, Tailwind o styled-components, importa los estilos en el layout donde los necesites. Evita hoja de estilos global que afecte a rutas que no deberían verse afectadas.
  • Prueba la jerarquía: abre la pestaña de Elements en las devtools del navegador y verifica que el árbol de elementos coincida con el orden esperado de layouts.

Errores comunes

  1. Olvidar exportar el componente por defecto en un layout: si el archivo no tiene un export default o un export const nombrado que Next.js reconozca, el layout será ignorado y verás solo el root layout.
  2. Colocar la lógica de datos dentro del layout y usar useEffect o useState sin marcar el componente como client component ('use client'). Esto provoca un error porque los layouts son server components por defecto.
  3. Anidar demasiados layouts sin razón: cada layout adicional añade un nivel de envoltorio y aumenta el tamaño del HTML enviado. Evalúa si realmente necesitas un layout nuevo o si puedes lograr lo mismo con CSS o con un componente reutilizable dentro de la página.
  4. Usar route groups para cambiar la URL: recuerda que los paréntesis en el nombre de la carpeta hacen que el segmento sea invisible en la URL. Si esperas que (panel)/ aparezca en la ruta, no funcionará.
  5. Sobrescribir accidentalmente el <html> o <body> en un layout interno: esto puede romper la inyección de metadata y de scripts de Next.js. Si necesitas cambiar atributos de esas etiquetas, hazlo solo en el root layout.
  6. No pasar children o renderizarlo más de una vez: si olvidas {children} o lo pones dentro de un condicional que nunca se cumple, la página aparecerá en blanco.

Conclusión

Los layouts anidados en el App Router de Next.js son una herramienta poderosa para construir interfaces consistentes y modulares. El root layout actúa como la capa más externa, enviando metadata y las etiquetas <html>/<body> a todas las rutas, mientras que los layouts de segmento permiten agregar capas específicas como barras laterales, encabezados de sección o contenedores de autenticación. Gracias a los route groups, puedes decidir exactamente qué rutas reciben ciertos layouts y cuál debe quedar excluido del envoltorio global.

Con los ejemplos vistos, ahora sabes cómo:

  • Crear un root layout básico que provea metadata y estructura básica.
  • Definir layouts de segmento que añadan funcionalidades como sidebar o formularios centrados.
  • Usar route groups para aislar secciones como autenticación o marketing.
  • Mantener el rendimiento y la claridad evitando lógica pesada en los layouts y respetando la convención de children.

Aplica estas ideas en tus próximos proyectos y experimenta con combinaciones de layouts, route groups y parallel routes para lograr la arquitectura de navegación que mejor se adapte a tus necesidades.

Próximos pasos

  • Explora parallel routes para mostrar varios componentes simultáneamente dentro de un mismo layout (por ejemplo, un modal o un sidebar que se abre sin cambiar la URL).
  • Investiga el uso de loading.js y error.js junto con los layouts para manejar estados de carga y error de forma granular.
  • Si tu aplicación necesita diferentes temas (claro/oscuro), prueba a pasar el tema como prop a través de los layouts usando React Context o una biblioteca de estado externo.

¡Con esto ya tenés una base sólida para dominar los layouts en el Next.js App Router y crear experiencias de usuario fluidas y bien estructuradas!

Comentarios (0)

Sé el primero en comentar.

Dejá tu comentario