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:
- Es obligatorio (NextJS da error si no existe)
- Debe incluir
<html>
y<body>
- Se aplica a todas las páginas de tu aplicación
- Es el único layout que puede modificar
<html>
y<body>
- 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
ysearchParams
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