
Migrar a Next.js 14/15 (App Router) no es solo una actualización de versión. No es como actualizar una dependencia donde revisas el changelog en busca de cambios que rompen compatibilidad. Es una migración a un framework completamente diferente.
Si has estado trabajando con React y Next.js tanto tiempo como yo—construyendo sistemas escalables para fintech o manejando plataformas de alto tráfico—desarrollas cierta memoria muscular. Sabes exactamente dónde va getServerSideProps. Sabes cómo _app.tsx envuelve tu lógica. Conoces los puntos exactos de dolor de "waterfall" en la obtención de datos del lado del cliente.
La parte más difícil de esta transición no es la nueva sintaxis; es el desaprendizaje arquitectónico requerido. En este artículo, quiero diseccionar el cambio fundamental del "Modelo Mental" del Pages Router (centrado en el cliente) al App Router (servidor primero).
Tabla de Contenidos
- El Mundo Antiguo: La Ilusión del "Cliente Grueso"
- La Nueva Realidad: React Server Components (RSC)
- El Código: Una Comparación Práctica
- Por qué esto importa (El Momento "¡Ajá!")
- La Heurística: Cuándo usar "use client"
- Pros, Contras y Verificaciones de Realidad
- Reflexiones Finales
- Referencias
El Mundo Antiguo: La Ilusión del "Cliente Grueso"
En Next.js 12 (Pages Router), vivíamos en un mundo donde el límite entre servidor y cliente era algo artificial. Escribíamos getServerSideProps o getStaticProps, que se ejecutaban en el servidor, pero el componente en sí—la parte de React—inevitablemente se enviaba al navegador para ser hidratado.
El modelo mental era:
- Servidor: Obtiene datos, renderiza string HTML.
- Red: Envía HTML + datos JSON + bundle de JavaScript.
- Cliente: Descarga JS, ejecuta React, hidrata el DOM y toma el control.
Esta arquitectura nos forzaba a una esquina específica de obtención de datos. Si necesitabas datos profundamente anidados en un árbol de componentes, tenías dos malas opciones:
- Prop Drilling: Obtenerlos en el nivel superior (Page) y pasarlos 10 capas hacia abajo.
- Obtención en Cliente: Renderizar el shell, luego usar
useEffect(o React Query) para obtener datos en el cliente, causando cambios de layout y waterfalls.
Pasamos años optimizando esto. Construimos gestión de estado compleja solo para mantener datos que estrictamente pertenecían a la UI. Aceptamos que nuestro "Frontend" era responsable de llamadas API distintas a nuestro backend.
La Nueva Realidad: React Server Components (RSC)
El App Router introduce React Server Components (RSC). Este es el cambio de paradigma.
En v15+, el componente por defecto es un Server Component. Se renderiza solo en el servidor. Nunca se hidrata. Su código nunca se envía al cliente.
Esto lo cambia todo.
En lugar de que tu componente sea una plantilla esperando datos, tu componente es el backend. Puede conectarse directamente a la base de datos. Puede leer el sistema de archivos. Puede mantener secretos.
El Código: Una Comparación Práctica
Veamos un requisito estándar: Obtener el perfil de un usuario y sus configuraciones del dashboard.
1. La Forma Next.js 12 (Pages Router)
En el modelo antiguo, separábamos la obtención del componente.
// pages/dashboard.tsx
import { GetServerSideProps } from 'next';
import { User, Settings } from '../types';
interface Props {
user: User;
settings: Settings;
}
export const getServerSideProps: GetServerSideProps<Props> = async (context) => {
// 1. La obtención ocurre aquí, desacoplada de la UI
const userRes = await fetch('https://api.internal/user', {
headers: { Cookie: context.req.headers.cookie || '' }
});
const settingsRes = await fetch(`https://api.internal/settings/${userRes.id}`);
const user = await userRes.json();
const settings = await settingsRes.json();
return {
props: {
user,
settings,
},
};
};
// 2. El componente recibe datos como props
export default function Dashboard({ user, settings }: Props) {
return (
<main>
<h1>Bienvenido, {user.name}</h1>
<SettingsPanel data={settings} />
</main>
);
}
La fricción: Si <SettingsPanel> necesita más datos después, tengo que editar getServerSideProps (el padre) y pasar las nuevas props hacia abajo. El componente no es autocontenido.
2. La Forma Next.js 15 (App Router)
En la pila moderna, colocamos los requisitos de datos junto con la UI. Marcamos el componente como async y esperamos datos directamente dentro de la función de renderizado.
// app/dashboard/page.tsx
import { cookies } from 'next/headers';
import { SettingsPanel } from './_components/settings-panel';
// Este es un Server Component por defecto
export default async function DashboardPage() {
// 1. Acceso directo a headers/cookies
const cookieStore = await cookies();
const token = cookieStore.get('auth_token');
// 2. Obtener datos directamente dentro del componente
const user = await getUser(token?.value);
return (
<main>
<h1>Bienvenido, {user.name}</h1>
{/* 3. Sin prop drilling para settings.
¡SettingsPanel puede obtener sus propios datos!
*/}
<SettingsPanel userId={user.id} />
</main>
);
}
// app/dashboard/_components/settings-panel.tsx
async function SettingsPanel({ userId }: { userId: string }) {
// Este componente obtiene sus propias dependencias en el servidor
const settings = await db.settings.findUnique({ where: { userId } });
return (
<section>
<h2>Configuración</h2>
<pre>{JSON.stringify(settings, null, 2)}</pre>
</section>
);
}
Por qué esto importa (El Momento "¡Ajá!")
Observa SettingsPanel. Obtiene sus propios datos. En Next.js 12, esto habría sido una obtención del lado del cliente (spinner de carga) o una pesadilla de prop drilling. En Next.js 15, React construye el resultado en el servidor y transmite el HTML. El navegador recibe el HTML completamente formado tanto para User como para Settings, con cero ejecución de JavaScript del lado del cliente requerida para esos datos.
La Heurística: Cuándo usar "use client"
La pregunta más común que recibo al mentorizar equipos en esta transición es: "Entonces, ¿nunca usamos componentes cliente?"
No. Los usas cuando necesitas interactividad, no para obtener datos.
Esta es la regla de decisión que uso en mis proyectos:
La Regla de Decisión:
- ¿Necesita leer datos (DB, API)? Server Component.
- ¿Necesita escuchar al usuario (onClick, onChange, useState)? Client Component.
Cuando necesitas interactividad, optas explícitamente agregando "use client" en la parte superior del archivo. Esto crea un "límite".
// app/components/like-button.tsx
"use client"; // <--- El Límite
import { useState } from 'react';
export default function LikeButton() {
const [likes, setLikes] = useState(0); // Los hooks solo funcionan aquí
return (
<button onClick={()=> setLikes(prev=> prev + 1)}>
Me gusta {likes}
</button>
);
}
Puedes entonces importar este <LikeButton /> en tu Server Component. El Server Component renderiza el HTML estático, y el Client Component se "hidrata" en una isla interactiva dentro de ese mar estático.
Pros, Contras y Verificaciones de Realidad
Los Pros
- Tamaño del Bundle: El código del Server Component nunca se envía al navegador. Bibliotecas grandes de formateo de fechas o parsers de markdown se quedan en el servidor.
- Seguridad: Puedes consultar tu DB directamente en el componente. No necesitas exponer una ruta API solo para poblar una vista.
- Modelo Mental: Los componentes se convierten en unidades autocontenidas de funcionalidad + datos, en lugar de solo plantillas de visualización.
Los Contras (Las "Trampas")
- Complejidad: Ahora tienes que rastrear mentalmente "¿Dónde se está renderizando esto?" Mezclar Server y Client components requiere atención estricta a los límites.
- Retraso del Ecosistema: No todas las bibliotecas de terceros soportan RSC aún (aunque la mayoría de las principales como kits de UI están alcanzando).
- Caché: Next.js 14/15 cachea agresivamente. En los viejos tiempos,
getServerSidePropsse ejecutaba en cada solicitud. En el App Router, las solicitudes fetch a menudo se cachean por defecto. Cubriremos esto más a fondo en el Capítulo 3.
Reflexiones Finales
El cambio al App Router se trata de mover el centro de gravedad del cliente de vuelta al servidor. Nos permite construir aplicaciones que se sienten como SPAs (Single Page Applications) pero tienen el rendimiento y la simplicidad de MPAs (Multi-Page Applications).
Se siente incómodo al principio. Intentarás usar useState en un Server Component y obtendrás un error. Lucharás con pasar funciones como props. Pero una vez que haga clic—una vez que veas el tamaño de tu bundle caer y tu flujo de datos simplificarse—no querrás volver.
En el próximo capítulo, abordaremos la Estrategia de Migración: cómo estructurar el directorio app/ y manejar los errores de "Hydration Mismatch" que inevitablemente aparecen durante la transición.