Páginas
Las páginas son los controladores de renderizado en Next.js que se encargan de orquestar features y manejar la lógica específica del enrutador. Su responsabilidad principal es controlar el flujo de renderizado sin importar qué están renderizando.
Principios Fundamentales
🚨 Regla Crítica: Las Páginas son SIEMPRE Server Components
Las páginas NUNCA pueden ser Client Components. Siempre son Server Components que:
- Se ejecutan en el servidor
- Pueden llamar a features, componentes aislados o cualquier JSX
- Orquestan el renderizado de componentes del servidor y del cliente
- Manejan la lógica específica del enrutador de Next.js
Responsabilidades Únicas de las Páginas
Las páginas NO contienen lógica de negocio. Su única responsabilidad es:
- Recibir parámetros del router (params, searchParams)
- Orquestar features y componentes apropiados
- Manejar estados de carga y error a nivel de página
- Controlar el layout y estructura de la página
- Ejecutar lógica de servidor (autenticación, redirects, etc.)
Anatomía de una Página
Estructura Básica - Server Component
// app/login/page.tsx
// ✅ SIEMPRE Server Component (sin 'use client')
import { LoginFeature } from '@/modules/auth/features/login';
interface LoginPageProps {
params: {};
searchParams: { [key: string]: string | string[] | undefined };
}
// ✅ Server Component por defecto - se ejecuta en el servidor
export default function LoginPage({ params, searchParams }: LoginPageProps) {
return (
<div className="login-page">
{/* ✅ Puede renderizar features (que pueden contener Client Components) */}
<LoginFeature />
</div>
);
}❌ Lo que NO Puede Hacer una Página
// ❌ NUNCA hacer esto - Las páginas NO pueden ser Client Components
'use client';
export default function LoginPage() {
const [state, setState] = useState(); // ❌ No puede usar hooks de cliente
return <div>...</div>;
}Componentes de una Página
1. Props del Router
Todas las páginas reciben automáticamente params y searchParams:
interface PageProps {
params: { [key: string]: string | string[] };
searchParams: { [key: string]: string | string[] | undefined };
}2. Orquestación de Features y Componentes
La página (Server Component) puede renderizar cualquier combinación de:
// app/dashboard/page.tsx
// ✅ Server Component que orquesta diferentes tipos de renderizado
import { DashboardFeature } from '@/modules/dashboard/features/dashboard';
import { StatsFeature } from '@/modules/analytics/features/stats';
import { WelcomeHeader } from '@/shared/components/welcome-header';
import { QuickActions } from '@/shared/components/quick-actions';
export default function DashboardPage() {
return (
<div className="dashboard-page">
{/* ✅ Componente aislado (Server Component) */}
<WelcomeHeader />
{/* ✅ Features modulares */}
<StatsFeature />
<DashboardFeature />
{/* ✅ Componente aislado que puede contener Client Components */}
<QuickActions />
{/* ✅ JSX directo */}
<footer className="dashboard-footer">
<p>© 2024 Creative Minds</p>
</footer>
</div>
);
}3. Manejo de Estados
Control de loading, error y empty states:
// app/products/page.tsx
import { Suspense } from 'react';
import { ProductsFeature } from '@/modules/products/features/products';
import { ProductsLoadingFallback } from '@/modules/products/fallbacks/loading';
export default function ProductsPage() {
return (
<div className="products-page">
<h1>Productos</h1>
<Suspense fallback={<ProductsLoadingFallback />}>
<ProductsFeature />
</Suspense>
</div>
);
}Qué Pueden Renderizar las Páginas
Como Server Components, las páginas pueden orquestar cualquier tipo de renderizado:
1. Features Completas
// ✅ Features que pueden contener Server y Client Components
import { CheckoutFeature } from '@/modules/payments/features/checkout';
export default function CheckoutPage() {
return <CheckoutFeature />;
}2. Componentes Aislados
// ✅ Componentes individuales del sistema de diseño
import { Hero } from '@/shared/components/hero';
import { ContactForm } from '@/shared/components/contact-form';
export default function ContactPage() {
return (
<div>
<Hero title="Contacto" />
<ContactForm />
</div>
);
}3. Combinación de Features y Componentes
// ✅ Mezcla de features, componentes y JSX
import { ProductsFeature } from '@/modules/products/features/products';
import { NewsletterSignup } from '@/shared/components/newsletter-signup';
export default function HomePage() {
return (
<div>
{/* JSX directo */}
<section className="hero">
<h1>Bienvenido</h1>
</section>
{/* Feature modular */}
<ProductsFeature />
{/* Componente aislado */}
<NewsletterSignup />
</div>
);
}4. Layouts Dinámicos
// ✅ Control dinámico del layout basado en datos del servidor
import { getDashboardLayout } from '@/modules/dashboard/services';
import { DashboardFeature } from '@/modules/dashboard/features/dashboard';
export default async function DashboardPage() {
const layout = await getDashboardLayout();
if (layout.type === 'compact') {
return (
<div className="compact-layout">
<DashboardFeature variant="compact" />
</div>
);
}
return (
<div className="full-layout">
<DashboardFeature variant="full" />
</div>
);
}Patrones de Páginas
1. Página Simple - Renderiza una Feature
// app/about/page.tsx
import { AboutFeature } from '@/features/about';
export default function AboutPage() {
return <AboutFeature />;
}2. Página con Parámetros - Pasa datos del router
// app/products/[id]/page.tsx
import { ProductDetailFeature } from '@/modules/products/features/product-detail';
interface ProductPageProps {
params: { id: string };
}
export default function ProductPage({ params }: ProductPageProps) {
return (
<div className="product-page">
<ProductDetailFeature productId={params.id} />
</div>
);
}3. Página con Query Parameters - Maneja filtros y búsquedas
// app/search/page.tsx
import { SearchFeature } from '@/features/search';
interface SearchPageProps {
searchParams: {
q?: string;
category?: string;
page?: string;
};
}
export default function SearchPage({ searchParams }: SearchPageProps) {
return (
<div className="search-page">
<SearchFeature
query={searchParams.q}
category={searchParams.category}
page={searchParams.page ? parseInt(searchParams.page) : 1}
/>
</div>
);
}4. Página Compuesta - Múltiples Features
// app/dashboard/analytics/page.tsx
import { Suspense } from 'react';
import { AnalyticsOverviewFeature } from '@/modules/analytics/features/overview';
import { ReportsFeature } from '@/modules/analytics/features/reports';
import { ChartsFeature } from '@/modules/analytics/features/charts';
export default function AnalyticsPage() {
return (
<div className="analytics-page">
<div className="page-header">
<h1>Analytics Dashboard</h1>
</div>
<div className="analytics-grid">
<Suspense fallback={<div>Cargando overview...</div>}>
<AnalyticsOverviewFeature />
</Suspense>
<Suspense fallback={<div>Cargando reportes...</div>}>
<ReportsFeature />
</Suspense>
<Suspense fallback={<div>Cargando gráficos...</div>}>
<ChartsFeature />
</Suspense>
</div>
</div>
);
}5. Página con Validación de Parámetros
// app/users/[id]/edit/page.tsx
import { notFound } from 'next/navigation';
import { UserEditFeature } from '@/modules/users/features/user-edit';
interface UserEditPageProps {
params: { id: string };
}
export default function UserEditPage({ params }: UserEditPageProps) {
// Validación simple de parámetros
const userId = params.id;
if (!userId || userId === 'undefined') {
notFound();
}
return (
<div className="user-edit-page">
<UserEditFeature userId={userId} />
</div>
);
}6. Página con Lógica de Servidor (Async Server Component)
// app/profile/page.tsx
// ✅ Server Component asíncrono - puede ejecutar lógica de servidor
import { redirect } from 'next/navigation';
import { getCurrentUser } from '@/modules/auth/services';
import { ProfileFeature } from '@/modules/users/features/profile';
import { ProfileHeader } from '@/shared/components/profile-header';
export default async function ProfilePage() {
// ✅ Lógica de servidor - autenticación, validación, redirects
const user = await getCurrentUser();
if (!user) {
redirect('/login');
}
if (!user.profileCompleted) {
redirect('/onboarding');
}
return (
<div className="profile-page">
{/* ✅ Componente aislado con datos del servidor */}
<ProfileHeader user={user} />
{/* ✅ Feature que puede contener Client Components */}
<ProfileFeature user={user} />
</div>
);
}Manejo de Estados en Páginas
1. Loading States con Suspense
// app/orders/page.tsx
import { Suspense } from 'react';
import { OrdersFeature } from '@/modules/orders/features/orders';
export default function OrdersPage() {
return (
<div className="orders-page">
<h1>Mis Pedidos</h1>
<Suspense
fallback={
<div className="loading-container">
<div className="spinner" />
<p>Cargando pedidos...</p>
</div>
}
>
<OrdersFeature />
</Suspense>
</div>
);
}2. Error Boundaries
// app/payments/error.tsx
'use client';
export default function PaymentsError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div className="error-page">
<h2>Error en Pagos</h2>
<p>Ocurrió un error al cargar la información de pagos.</p>
<button onClick={reset}>Intentar de nuevo</button>
</div>
);
}3. Not Found Pages
// app/products/[id]/not-found.tsx
export default function ProductNotFound() {
return (
<div className="not-found-page">
<h2>Producto no encontrado</h2>
<p>El producto que buscas no existe o ha sido eliminado.</p>
<a href="/products">Ver todos los productos</a>
</div>
);
}Patrones Avanzados
1. Página con Múltiples Layouts
// app/(dashboard)/analytics/page.tsx
import { DashboardLayout } from '@/shared/layouts/dashboard-layout';
import { AnalyticsFeature } from '@/modules/analytics/features/analytics';
export default function AnalyticsPage() {
return (
<DashboardLayout>
<AnalyticsFeature />
</DashboardLayout>
);
}2. Página con Datos Pre-cargados
// app/blog/[slug]/page.tsx
import { getBlogPost } from '@/modules/blog/services';
import { BlogPostFeature } from '@/modules/blog/features/blog-post';
import { notFound } from 'next/navigation';
interface BlogPostPageProps {
params: { slug: string };
}
export default async function BlogPostPage({ params }: BlogPostPageProps) {
const post = await getBlogPost(params.slug);
if (!post) {
notFound();
}
return (
<article className="blog-post-page">
<BlogPostFeature post={post} />
</article>
);
}
// Generar metadata dinámicamente
export async function generateMetadata({ params }: BlogPostPageProps) {
const post = await getBlogPost(params.slug);
return {
title: post?.title || 'Post no encontrado',
description: post?.excerpt,
};
}3. Página con Streaming
// app/dashboard/page.tsx
import { Suspense } from 'react';
import { DashboardOverview } from '@/modules/dashboard/features/overview';
import { RecentActivity } from '@/modules/dashboard/features/recent-activity';
import { QuickStats } from '@/modules/dashboard/features/quick-stats';
export default function DashboardPage() {
return (
<div className="dashboard-page">
{/* Se carga inmediatamente */}
<div className="dashboard-header">
<h1>Dashboard</h1>
</div>
{/* Se carga de forma independiente */}
<div className="dashboard-grid">
<Suspense fallback={<QuickStatsSkeleton />}>
<QuickStats />
</Suspense>
<Suspense fallback={<OverviewSkeleton />}>
<DashboardOverview />
</Suspense>
<Suspense fallback={<ActivitySkeleton />}>
<RecentActivity />
</Suspense>
</div>
</div>
);
}Mejores Prácticas para Páginas
1. Mantener Páginas como Server Components
// ✅ Correcto - Server Component que orquesta features
export default function LoginPage() {
return <LoginFeature />;
}
// ✅ Correcto - Server Component con lógica de servidor
export default async function LoginPage() {
const user = await getCurrentUser();
if (user) {
redirect('/dashboard');
}
return <LoginFeature />;
}
// ❌ Incorrecto - Client Component (NUNCA hacer esto)
'use client';
export default function LoginPage() {
const [email, setEmail] = useState(''); // ❌ No puede usar hooks de cliente
return <form>...</form>;
}2. Usar Props Tipadas
// ✅ Correcto - Props bien tipadas
interface UserPageProps {
params: { id: string };
searchParams: { tab?: string };
}
export default function UserPage({ params, searchParams }: UserPageProps) {
return (
<UserFeature
userId={params.id}
activeTab={searchParams.tab}
/>
);
}3. Validar Parámetros Críticos
// ✅ Correcto - Validación de parámetros
export default function ProductPage({ params }: { params: { id: string } }) {
if (!params.id || params.id === 'undefined') {
notFound();
}
return <ProductFeature productId={params.id} />;
}4. Usar Suspense para Loading States
// ✅ Correcto - Suspense para cargas asíncronas
export default function OrdersPage() {
return (
<Suspense fallback={<OrdersLoadingSkeleton />}>
<OrdersFeature />
</Suspense>
);
}5. Separar Concerns - Server vs Client
// ✅ Correcto - Server Component que orquesta todo
export default function CheckoutPage() {
return (
<div className="checkout-page">
{/* Server Components para layout */}
<CheckoutHeader />
{/* Feature que puede contener Client Components internamente */}
<CheckoutFeature />
{/* Server Component para footer */}
<CheckoutFooter />
</div>
);
}6. Aprovechar las Capacidades del Server Component
// ✅ Correcto - Usar capacidades del servidor
export default async function ProductPage({ params }: { params: { id: string } }) {
// ✅ Fetch de datos en el servidor
const product = await getProduct(params.id);
// ✅ Lógica de servidor
if (!product) {
notFound();
}
// ✅ Generar contenido dinámico en el servidor
const relatedProducts = await getRelatedProducts(product.category);
return (
<div>
{/* ✅ Pasar datos del servidor a features/componentes */}
<ProductDetailFeature product={product} />
<RelatedProductsFeature products={relatedProducts} />
</div>
);
}Estructura de Archivos de Páginas
Organización Recomendada
app/
├── (auth)/ # Grupo de rutas
│ ├── login/
│ │ └── page.tsx # Página de login
│ ├── register/
│ │ └── page.tsx # Página de registro
│ └── layout.tsx # Layout para auth
├── (dashboard)/ # Grupo de rutas
│ ├── analytics/
│ │ ├── page.tsx # Página principal
│ │ ├── loading.tsx # Loading UI
│ │ └── error.tsx # Error UI
│ ├── users/
│ │ ├── page.tsx
│ │ └── [id]/
│ │ ├── page.tsx # Detalle de usuario
│ │ ├── edit/
│ │ │ └── page.tsx # Editar usuario
│ │ └── not-found.tsx
│ └── layout.tsx # Layout para dashboard
├── blog/
│ ├── page.tsx # Lista de posts
│ └── [slug]/
│ ├── page.tsx # Post individual
│ └── loading.tsx
├── api/ # API routes
├── globals.css
├── layout.tsx # Root layout
└── page.tsx # Home pageComunicación Página-Feature
Reglas de Serialización: Qué Pueden Pasar las Páginas
Como Server Components, las páginas solo pueden pasar datos serializables a las features:
✅ Datos Permitidos (Serializables)
// app/products/page.tsx
export default async function ProductsPage({ params, searchParams }) {
const serverData = await getProductsData();
return (
<ProductsFeature
// ✅ Strings
category={searchParams.category}
sortBy={searchParams.sort}
// ✅ Numbers
page={searchParams.page ? parseInt(searchParams.page) : 1}
// ✅ Booleans
isAdmin={true}
// ✅ Objects (serializables)
filters={{ category: 'tech', price: 100 }}
// ✅ Arrays
categories={['tech', 'design', 'business']}
// ✅ Datos del servidor
serverData={serverData}
// ✅ Null/undefined
selectedItem={null}
/>
);
}❌ Datos NO Permitidos (No Serializables)
// ❌ INCORRECTO - Estos NO se pueden pasar
export default function ProductsPage() {
return (
<ProductsFeature
// ❌ Funciones
onFilter={() => {}}
onClick={handleClick}
// ❌ Objetos con métodos
utils={{ format: () => {} }}
// ❌ Clases
service={new ProductService()}
// ❌ Symbols
key={Symbol('key')}
// ❌ Dates (usar strings ISO)
createdAt={new Date()} // Usar createdAt={new Date().toISOString()}
/>
);
}1. Pasar Datos del Router y Servidor
// app/products/page.tsx
export default async function ProductsPage({ params, searchParams }) {
// ✅ Datos del servidor
const categories = await getCategories();
const userPreferences = await getUserPreferences();
return (
<ProductsFeature
// ✅ Parámetros del router
category={searchParams.category}
sortBy={searchParams.sort}
page={searchParams.page ? parseInt(searchParams.page) : 1}
// ✅ Datos del servidor
availableCategories={categories}
userPreferences={userPreferences}
/>
);
}2. Pasar Datos del Servidor a Features
// app/cart/page.tsx
// ✅ Correcto - Server Component que pasa datos, no funciones
export default async function CartPage({ searchParams }: {
searchParams: { coupon?: string }
}) {
// ✅ Lógica de servidor
const cartData = await getCartData();
const appliedCoupon = searchParams.coupon ?
await validateCoupon(searchParams.coupon) : null;
return (
<div className="cart-page">
<div className="cart-header">
<h1>Carrito de Compras</h1>
</div>
<Suspense fallback={<CartLoadingSkeleton />}>
{/* ✅ Solo pasar datos serializables, no funciones */}
<CartFeature
cartData={cartData}
appliedCoupon={appliedCoupon}
/>
</Suspense>
</div>
);
}❌ Lo que NO se puede hacer:
// ❌ INCORRECTO - Server Components no pueden pasar funciones
export default function CartPage() {
return (
<CartFeature
onCheckout={() => {}} // ❌ Error: funciones no son serializables
onClick={handleClick} // ❌ Error: funciones no son serializables
/>
);
}3. Las Features Manejan Su Propia Interactividad
// ✅ Correcto - La feature maneja sus propios eventos y estado
// app/checkout/page.tsx
export default async function CheckoutPage() {
const cartItems = await getCartItems();
return (
<div className="checkout-page">
{/* ✅ Solo pasar datos, la feature maneja la interactividad */}
<CheckoutFeature
items={cartItems}
// La feature internamente manejará:
// - onSubmit con Server Actions
// - onChange con Client Components
// - onError con error boundaries
/>
</div>
);
}
// Dentro de CheckoutFeature (puede contener Client Components)
// src/modules/payments/features/checkout/components/_checkout-form.tsx
'use client';
import { processPaymentAction } from '@/modules/payments/actions';
export function CheckoutForm({ items }) {
// ✅ La feature maneja su propia interactividad
const handleSubmit = async (formData) => {
await processPaymentAction(formData);
};
return (
<form action={handleSubmit}>
{/* Formulario interactivo */}
</form>
);
}Beneficios del Patrón de Páginas como Server Components
1. Performance Óptima
- Zero JavaScript por defecto: Las páginas no envían JS al cliente
- Server-side rendering: Contenido generado en el servidor
- Streaming: Carga progresiva con Suspense
- Caching: Aprovecha el cache del servidor de Next.js
2. Acceso Completo al Servidor
// ✅ Acceso directo a recursos del servidor
export default async function DashboardPage() {
// Base de datos
const user = await db.user.findUnique({ where: { id: userId } });
// Variables de entorno
const apiKey = process.env.API_KEY;
// Sistema de archivos
const config = await fs.readFile('./config.json');
return <DashboardFeature user={user} />;
}3. SEO y Accesibilidad
- HTML completo en el servidor: Mejor SEO
- Metadata dinámica: Generada en el servidor
- Accesibilidad: Contenido disponible inmediatamente
4. Separación Clara de Responsabilidades
- Páginas (Server): Routing, datos del servidor, orchestración
- Features: Lógica de negocio, UI, interacción (Server + Client)
- Client Components: Solo interactividad específica
5. Reutilización Inteligente
Las features pueden usarse en diferentes contextos de servidor:
// app/admin/users/page.tsx - Server Component
export default async function AdminUsersPage() {
const adminUser = await getAdminUser();
return (
<AdminLayout>
<UsersFeature user={adminUser} adminMode={true} />
</AdminLayout>
);
}
// app/users/page.tsx - Server Component
export default async function UsersPage() {
const publicData = await getPublicUserData();
return (
<PublicLayout>
<UsersFeature data={publicData} adminMode={false} />
</PublicLayout>
);
}6. Testing Simplificado
- Páginas: Test de routing, datos del servidor, y rendering
- Features: Test de lógica de negocio e interacción
- Separación clara: Server logic vs Client logic
Las páginas actúan como orquestadores inteligentes del servidor que conectan el sistema de routing de Next.js con las features de la aplicación. Al ser siempre Server Components, aprovechan al máximo las capacidades del servidor mientras mantienen una arquitectura limpia, performante y escalable.
Resumen: Páginas como Server Components
✅ Lo que SÍ hacen las páginas:
- Server Components: Siempre se ejecutan en el servidor
- Orquestar renderizado: Features, componentes, JSX
- Manejar routing: params, searchParams, redirects
- Acceder al servidor: Base de datos, APIs, sistema de archivos
- Generar metadata: SEO dinámico
- Controlar layout: Estructura y composición
❌ Lo que NO hacen las páginas:
- Client Components: Nunca usan ‘use client’
- Hooks de cliente: useState, useEffect, etc.
- Lógica de negocio: Delegada a features y servicios
- Interactividad directa: Manejada por Client Components internos
🎯 Patrón Ideal:
// ✅ Página perfecta: Server Component que orquesta
export default async function IdealPage({ params, searchParams }) {
// Lógica de servidor
const data = await getServerData(params.id);
return (
<div>
{/* Orquestar diferentes tipos de renderizado */}
<ServerComponent data={data} />
<FeatureWithClientComponents filter={searchParams.filter} />
<AnotherFeature />
</div>
);
}