Pages y Layouts

Las páginas y layouts son los componentes fundamentales de tu aplicación NextJS. Las páginas definen el contenido de cada ruta, mientras que los layouts definen la estructura compartida entre múltiples páginas.

Pages - Contenido de las rutas

Un archivo page.tsx define el contenido único de una ruta. Es el componente que se renderiza cuando el usuario visita esa URL.

Estructura básica

// app/productos/page.tsx
export default function ProductosPage() {
  return (
    <div>
      <h1>Nuestros Productos</h1>
      <p>Explora nuestro catálogo completo</p>
    </div>
  )
}

Esta página se muestra cuando el usuario visita /productos.

Props de página

Las páginas reciben dos props automáticamente:

1. params - Parámetros de ruta

Contiene los segmentos dinámicos de la URL:

// app/productos/[id]/page.tsx
export default function ProductoDetallePage({
  params,
}: {
  params: { id: string }
}) {
  return <h1>Producto: {params.id}</h1>
}

Si visitas /productos/camisa-azul, entonces params.id será "camisa-azul".

Para rutas con múltiples segmentos dinámicos:

// app/categoria/[categoria]/producto/[id]/page.tsx
export default function Page({
  params,
}: {
  params: { categoria: string; id: string }
}) {
  return (
    <div>
      <h1>Categoría: {params.categoria}</h1>
      <h2>Producto: {params.id}</h2>
    </div>
  )
}

URL: /categoria/ropa/producto/123

  • params.categoria = "ropa"
  • params.id = "123"

2. searchParams - Query parameters

Contiene los parámetros de búsqueda de la URL (la parte después del ?):

// app/productos/page.tsx
export default function ProductosPage({
  searchParams,
}: {
  searchParams: { orden?: string; categoria?: string }
}) {
  return (
    <div>
      <h1>Productos</h1>
      <p>Orden: {searchParams.orden || 'ninguno'}</p>
      <p>Categoría: {searchParams.categoria || 'todas'}</p>
    </div>
  )
}

URL: /productos?orden=precio&categoria=ropa

  • searchParams.orden = "precio"
  • searchParams.categoria = "ropa"
⚠️
searchParams es una Promise en NextJS 15

En NextJS 15, searchParams es una Promise que debes await. Esto permite mejor optimización del servidor.

export default async function ProductosPage({
  searchParams,
}: {
  searchParams: Promise<{ orden?: string }>
}) {
  const params = await searchParams
  return <p>Orden: {params.orden}</p>
}

Páginas asíncronas

Las páginas pueden ser componentes async para hacer fetch de datos:

// app/productos/[id]/page.tsx
async function getProducto(id: string) {
  const res = await fetch(`https://api.ejemplo.com/productos/${id}`)
  return res.json()
}

export default async function ProductoPage({
  params,
}: {
  params: { id: string }
}) {
  const producto = await getProducto(params.id)
  
  return (
    <div>
      <h1>{producto.nombre}</h1>
      <p>{producto.descripcion}</p>
      <p>Precio: ${producto.precio}</p>
    </div>
  )
}

NextJS espera a que se resuelva el fetch antes de renderizar la página.

Layouts - Estructura compartida

Un layout.tsx define UI que se comparte entre múltiples páginas. Los layouts envuelven las páginas y persisten entre navegaciones, lo que significa que no se vuelven a renderizar al cambiar de página.

Layout básico

// app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="es">
      <body>
        <header>
          <nav>{/* Navegación */}</nav>
        </header>
        <main>{children}</main>
        <footer>{/* Footer */}</footer>
      </body>
    </html>
  )
}

El prop children contiene la página actual o el layout anidado.

Root Layout - Layout raíz

El layout en app/layout.tsx es especial: se llama Root Layout (layout raíz) y es obligatorio. Debe contener las etiquetas <html> y <body>.

// app/layout.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'Mi Tienda',
  description: 'La mejor tienda online',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="es">
      <body>
        {children}
      </body>
    </html>
  )
}

Características del Root Layout:

  1. Es obligatorio (NextJS da error si no existe)
  2. Debe incluir <html> y <body>
  3. Se aplica a todas las páginas de tu aplicación
  4. Es el único layout que puede modificar <html> y <body>
  5. Puede exportar metadata global
⚠️

Solo el Root Layout puede contener <html> y <body>. Los layouts anidados no deben incluir estas etiquetas.

Layouts anidados

Puedes crear layouts dentro de rutas específicas para compartir UI solo entre páginas relacionadas.

Ejemplo: Layout de tienda

// app/tienda/layout.tsx
export default function TiendaLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div className="tienda-container">
      <aside className="sidebar">
        <h2>Categorías</h2>
        <ul>
          <li>Ropa</li>
          <li>Zapatos</li>
          <li>Accesorios</li>
        </ul>
      </aside>
      <div className="contenido">
        {children}
      </div>
    </div>
  )
}

Este layout se aplica a todas las rutas dentro de /tienda/*:

app/
├── layout.tsx                    ← Root Layout (global)
└── tienda/
    ├── layout.tsx                ← Layout de tienda
    ├── page.tsx                  ← /tienda (usa layout de tienda)
    ├── productos/
    │   └── page.tsx              ← /tienda/productos (usa layout de tienda)
    └── carrito/
        └── page.tsx              ← /tienda/carrito (usa layout de tienda)

Jerarquía de layouts

Los layouts se anidan uno dentro de otro. Si tienes múltiples layouts en la ruta, se envuelven en orden:

app/
├── layout.tsx                    ← Layout 1 (root)
└── tienda/
    ├── layout.tsx                ← Layout 2
    └── productos/
        ├── layout.tsx            ← Layout 3
        └── page.tsx              ← Página

Resultado al visitar /tienda/productos:

<RootLayout>                      {/* app/layout.tsx */}
  <TiendaLayout>                  {/* app/tienda/layout.tsx */}
    <ProductosLayout>             {/* app/tienda/productos/layout.tsx */}
      <ProductosPage />           {/* app/tienda/productos/page.tsx */}
    </ProductosLayout>
  </TiendaLayout>
</RootLayout>

Cada layout envuelve al siguiente nivel.

Ejemplo completo - E-commerce

Estructura de layouts para una tienda online:

// app/layout.tsx - Root Layout
import './globals.css'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="es">
      <body>
        <header>
          <nav>
            <a href="/">Inicio</a>
            <a href="/tienda">Tienda</a>
            <a href="/contacto">Contacto</a>
          </nav>
        </header>
        {children}
        <footer>
          <p>© 2025 Mi Tienda</p>
        </footer>
      </body>
    </html>
  )
}
// app/tienda/layout.tsx - Layout de tienda
export default function TiendaLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div className="tienda">
      <aside className="filtros">
        <h3>Filtrar por:</h3>
        <div>
          <h4>Categoría</h4>
          <label><input type="checkbox" /> Ropa</label>
          <label><input type="checkbox" /> Zapatos</label>
        </div>
        <div>
          <h4>Precio</h4>
          <label><input type="checkbox" /> Menos de $50</label>
          <label><input type="checkbox" /> $50 - $100</label>
        </div>
      </aside>
      <main className="contenido">
        {children}
      </main>
    </div>
  )
}
// app/tienda/productos/page.tsx - Página de productos
export default function ProductosPage() {
  return (
    <div>
      <h1>Todos los Productos</h1>
      <div className="grid">
        {/* Lista de productos */}
      </div>
    </div>
  )
}

Cuando visitas /tienda/productos, la estructura renderizada es:

Header (del Root Layout)
  Filtros (del Tienda Layout)
  Contenido:
    - Todos los Productos (de la página)
Footer (del Root Layout)

Templates - Alternativa a Layouts

Los templates son similares a los layouts pero con una diferencia clave: se vuelven a crear en cada navegación.

Diferencias entre Layout y Template

Layout:

  • Persiste entre navegaciones
  • El estado se mantiene
  • No se vuelve a montar
  • Mejor rendimiento

Template:

  • Se recrea en cada navegación
  • El estado se resetea
  • Se vuelve a montar
  • Útil para animaciones de entrada/salida

Cuándo usar Template

// app/tienda/template.tsx
'use client'

import { motion } from 'framer-motion'

export default function TiendaTemplate({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      exit={{ opacity: 0, y: -20 }}
    >
      {children}
    </motion.div>
  )
}

Este template aplica una animación cada vez que navegas a una nueva página dentro de /tienda/*.

ℹ️

Los templates deben ser Client Components ('use client') si usan hooks o interactividad. Los layouts pueden ser Server Components por defecto.

Jerarquía con Templates

Si tienes tanto layout como template:

app/
└── tienda/
    ├── layout.tsx
    ├── template.tsx
    └── page.tsx

Se renderiza así:

<TiendaLayout>               {/* Persiste */}
  <TiendaTemplate>           {/* Se recrea */}
    <ProductosPage />
  </TiendaTemplate>
</TiendaLayout>

El layout persiste, pero el template se recrea en cada navegación.

Compartir datos entre Layouts y Pages

Pasar props - NO funciona

No puedes pasar props directamente de un layout a una página:

// ❌ Esto NO funciona
export default function Layout({ children }) {
  return children({ usuario: 'Juan' })  // No puedes pasar props así
}

Soluciones para compartir datos

1. Fetch en múltiples lugares

NextJS automáticamente deduplica requests idénticas:

// app/tienda/layout.tsx
async function getUsuario() {
  const res = await fetch('https://api.ejemplo.com/usuario')
  return res.json()
}

export default async function TiendaLayout({ children }) {
  const usuario = await getUsuario()
  return (
    <div>
      <p>Bienvenido, {usuario.nombre}</p>
      {children}
    </div>
  )
}
// app/tienda/productos/page.tsx
async function getUsuario() {
  const res = await fetch('https://api.ejemplo.com/usuario')
  return res.json()
}

export default async function ProductosPage() {
  const usuario = await getUsuario()
  return <p>Recomendaciones para {usuario.nombre}</p>
}

Ambos hacen fetch de getUsuario(), pero NextJS solo hace UNA petición HTTP y cachea el resultado.

2. React Context (para Client Components)

// app/tienda/layout.tsx
'use client'

import { createContext } from 'react'

export const TiendaContext = createContext(null)

export default function TiendaLayout({ children }) {
  const datos = { carrito: [], usuario: 'Juan' }
  
  return (
    <TiendaContext.Provider value={datos}>
      {children}
    </TiendaContext.Provider>
  )
}
// app/tienda/productos/page.tsx
'use client'

import { useContext } from 'react'
import { TiendaContext } from '../layout'

export default function ProductosPage() {
  const { usuario } = useContext(TiendaContext)
  return <p>Hola, {usuario}</p>
}

Metadata en Layouts y Pages

Tanto layouts como pages pueden exportar metadata para SEO.

Metadata en Layout

// app/tienda/layout.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: {
    template: '%s | Mi Tienda',
    default: 'Mi Tienda',
  },
  description: 'La mejor tienda online',
}

export default function TiendaLayout({ children }) {
  return <div>{children}</div>
}

El template permite que las páginas hijas agreguen su título:

// app/tienda/productos/page.tsx
export const metadata = {
  title: 'Productos',  // Se convierte en "Productos | Mi Tienda"
}

Metadata en Page sobrescribe Layout

// app/tienda/layout.tsx
export const metadata = {
  title: 'Tienda',
  description: 'Descripción del layout',
}

// app/tienda/productos/page.tsx
export const metadata = {
  title: 'Productos',
  description: 'Descripción de productos',
}

La página /tienda/productos usa los valores de la page, no del layout.

Modificar <head> - Solo con metadata

No puedes agregar etiquetas directamente en <head>:

// ❌ Esto NO funciona
export default function Layout() {
  return (
    <html>
      <head>
        <title>Mi sitio</title>  {/* No hagas esto */}
      </head>
      <body>...</body>
    </html>
  )
}

Usa metadata en su lugar:

// ✓ Correcto
export const metadata = {
  title: 'Mi sitio',
}

NextJS genera automáticamente las etiquetas <head> correctas.

Layouts y Suspense boundaries

Los layouts crean automáticamente Suspense boundaries para loading.tsx:

app/
└── tienda/
    ├── layout.tsx
    ├── loading.tsx
    └── productos/
        └── page.tsx

Cuando productos/page.tsx está cargando:

<TiendaLayout>
  <Suspense fallback={<LoadingUI />}>  {/* Automático */}
    <ProductosPage />
  </Suspense>
</TiendaLayout>

El layout permanece visible mientras la página carga.

Mejores prácticas

1. Mantén layouts simples

Los layouts deben ser estructura, no lógica compleja:

// ✓ Layout simple
export default function Layout({ children }) {
  return (
    <div className="container">
      <Sidebar />
      <main>{children}</main>
    </div>
  )
}

// ✗ Layout complejo (mueve esto a componentes)
export default function Layout({ children }) {
  const [estado, setEstado] = useState()
  const datos = useFetch()
  // ... mucha lógica
  return <div>...</div>
}

2. Usa Server Components por defecto

Los layouts pueden ser Server Components a menos que necesites interactividad:

// ✓ Server Component (por defecto)
export default function Layout({ children }) {
  return <div>{children}</div>
}

// Solo usa 'use client' si necesitas hooks
'use client'
export default function Layout({ children }) {
  const [estado] = useState()
  return <div>{children}</div>
}

3. Coloca navegación en Root Layout

// app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Nav />          {/* Navegación global */}
        {children}
        <Footer />       {/* Footer global */}
      </body>
    </html>
  )
}

4. Layouts específicos para secciones

// app/(admin)/layout.tsx - Solo para admin
export default function AdminLayout({ children }) {
  return (
    <div>
      <AdminSidebar />
      {children}
    </div>
  )
}

// app/(publico)/layout.tsx - Solo para público
export default function PublicoLayout({ children }) {
  return (
    <div>
      <Header />
      {children}
    </div>
  )
}

5. Fetch de datos en el nivel correcto

Fetch datos tan cerca de donde se usan como sea posible:

// ✓ Fetch en la página que los necesita
export default async function ProductosPage() {
  const productos = await getProductos()
  return <Lista productos={productos} />
}

// ✗ No fetches todo en el layout
export default async function Layout({ children }) {
  const productos = await getProductos()  // No todos lo necesitan
  const usuarios = await getUsuarios()    // No todos lo necesitan
  return <div>{children}</div>
}

6. Un layout por propósito

No anides demasiados layouts sin razón:

// ✗ Demasiados layouts
app/tienda/layout.tsx
app/tienda/productos/layout.tsx
app/tienda/productos/lista/layout.tsx
app/tienda/productos/lista/grid/layout.tsx

// ✓ Solo layouts necesarios
app/tienda/layout.tsx
app/tienda/productos/layout.tsx

Casos de uso comunes

Dashboard con sidebar

// app/dashboard/layout.tsx
export default function DashboardLayout({ children }) {
  return (
    <div className="flex">
      <aside className="w-64 bg-gray-100">
        <nav>
          <a href="/dashboard">Inicio</a>
          <a href="/dashboard/analytics">Analytics</a>
          <a href="/dashboard/settings">Settings</a>
        </nav>
      </aside>
      <main className="flex-1">
        {children}
      </main>
    </div>
  )
}

Autenticación requerida

// app/(privado)/layout.tsx
import { redirect } from 'next/navigation'
import { getUsuarioSesion } from '@/lib/auth'

export default async function PrivadoLayout({ children }) {
  const usuario = await getUsuarioSesion()
  
  if (!usuario) {
    redirect('/login')
  }
  
  return (
    <div>
      <p>Bienvenido, {usuario.nombre}</p>
      {children}
    </div>
  )
}

Todas las rutas dentro de (privado)/ requieren autenticación.

Layout sin header/footer

// app/(auth)/layout.tsx
export default function AuthLayout({ children }) {
  return (
    <div className="min-h-screen flex items-center justify-center">
      {children}
    </div>
  )
}

Las páginas de login/registro no muestran el header/footer del Root Layout.

Resumen

Pages:

  • Definen el contenido único de cada ruta
  • Reciben params y searchParams como props
  • Pueden ser componentes async
  • Se renderizan dentro de layouts

Layouts:

  • Definen estructura compartida
  • Envuelven páginas y otros layouts
  • Persisten entre navegaciones
  • El Root Layout es obligatorio y único

Templates:

  • Como layouts pero se recrean en cada navegación
  • Útiles para animaciones
  • Resetean estado

Jerarquía:

Root Layout (obligatorio)
  └─ Layout anidado
      └─ Template
          └─ Page

Reglas clave:

  • Solo Root Layout puede tener <html> y <body>
  • No puedes pasar props entre layouts y pages
  • Usa metadata para modificar <head>
  • Layouts persisten, templates se recrean