La razón más común por la que SSR no funciona en Next.js App Router: Next.js optimiza automáticamente las páginas a generación estática (SSG). Si tu página no usa APIs dinámicas como cookies() o headers(), se pre-renderiza como HTML estático en tiempo de build — sin ejecución por solicitud. Soluciónalo con export const dynamic = "force-dynamic" o cache: "no-store".
Mientras construía 32blog.com, me topé con esto varias veces: "Los datos están actualizados — ¿por qué no aparecen?" Redesplegar no ayudó. Limpiar cachés no ayudó.
Casi siempre, la causa era una mala comprensión de la estrategia de renderizado de App Router. El patrón más común: "Pensé que estaba usando SSR, pero Next.js cambió silenciosamente a generación estática."
Este artículo recorre los patrones típicos donde SSR no se comporta como esperas en App Router, con soluciones para cada uno.
Entendiendo las estrategias de renderizado de App Router
Primero, los fundamentos. Las páginas de App Router funcionan bajo una de tres estrategias:
| Estrategia | Cuándo | Caso de uso |
|---|---|---|
| Generación estática (SSG) | Una vez en tiempo de build | Contenido que no cambia |
| Renderizado dinámico (SSR) | En cada solicitud | Datos por usuario o en tiempo real |
| ISR | Regeneración periódica | Contenido que se actualiza ocasionalmente |
El problema es que Next.js optimiza automáticamente a generación estática cuando cree que puede. Las páginas que pretendes que sean dinámicas se vuelven estáticas silenciosamente.
¿Agregar "use client" habilita SSR?
Primero desmintamos un mito: "use client" no tiene nada que ver con SSR.
"use client" marca un componente como Client Component. Define los límites entre Server y Client Component — no significa "no ejecutes esto en el servidor."
// Concepto erróneo común: "use client" significa solo navegador
"use client";
import { useState, useEffect } from "react";
export function UserProfile() {
const [user, setUser] = useState(null);
useEffect(() => {
fetch("/api/user").then(r => r.json()).then(setUser);
}, []);
if (!user) return <p>Loading...</p>;
return <p>{user.name}</p>;
}
Eso es fetching del lado del cliente — nada que ver con SSR. Los datos del servidor no se obtienen. El SEO también se ve afectado.
Para obtener datos realmente con SSR, escribe un Server Component asíncrono:
// Server Component data fetching (SSR/SSG)
// Sin "use client" = Server Component
export default async function UserProfile() {
// Se ejecuta en el servidor
const user = await getUser();
return <p>{user.name}</p>;
}
¿Por qué Next.js cambia a generación estática?
Next.js optimiza a generación estática cuando todo lo siguiente es verdad:
- No se usan APIs dinámicas (
cookies(),headers(), etc.) - No se accede a
searchParams - No hay opciones de fetch que invaliden el caché
- No se establece
export const dynamic = "force-dynamic"
Un Server Component simple con data fetching le parece a Next.js "puedo ejecutar esto en tiempo de build y cachearlo" — así que se convierte en SSG automáticamente.
Cómo verificar: lee el log de build
npm run build
El log de build muestra la estrategia de renderizado de cada página:
Route (app) Size First Load JS
┌ ○ / 4.2 kB 120 kB
├ ○ /[locale]/about 2.1 kB 118 kB
├ ƒ /[locale]/dashboard 5.3 kB 121 kB
└ ● /[locale]/blog/[slug] 8.4 kB 124 kB
○— Generación estática (SSG)ƒ— Renderizado dinámico (SSR)●— ISR (estático con revalidación)
Si una página muestra ○ pero esperas que sea dinámica, ahí está tu problema de SSR.
Solución 1: force-dynamic para renderizado dinámico garantizado
La solución más simple. Agrega un export a tu archivo de página.
// app/[locale]/dashboard/page.tsx
// Agrega esto — eso es todo lo que necesitas para forzar SSR
export const dynamic = "force-dynamic";
export default async function DashboardPage() {
// Se ejecuta en cada solicitud
const data = await fetchDashboardData();
return <Dashboard data={data} />;
}
Ten en cuenta que force-dynamic significa que el procesamiento del servidor se ejecuta en cada solicitud, lo que aumenta la carga. Úsalo solo donde realmente sea necesario.
Solución 2: Deshabilitar explícitamente el caché de fetch
Next.js 15+ usa no-store por defecto, así que esto es menos común ahora. Pero si force-cache termina en tu código, anula el valor predeterminado.
// Pretendía ser SSR, pero force-cache lo fija en tiempo de build
export default async function PricePage() {
const prices = await fetch("https://api.example.com/prices", {
cache: "force-cache", // Cacheado en tiempo de build → nunca se actualiza
}).then(r => r.json());
return <PriceTable prices={prices} />;
}
// Siempre obtener datos frescos
export default async function PricePage() {
const prices = await fetch("https://api.example.com/prices", {
cache: "no-store", // Sin caché → se obtiene fresco en cada solicitud
}).then(r => r.json());
return <PriceTable prices={prices} />;
}
Establecer no-store en cualquier fetch automáticamente hace que la página contenedora sea dinámica. No necesitas establecer force-dynamic por separado.
Solución 3: Usar APIs dinámicas para activar SSR
Llamar a cookies() o headers() le indica a Next.js que la página debe renderizarse dinámicamente.
// app/[locale]/profile/page.tsx
import { cookies } from "next/headers";
export default async function ProfilePage() {
// Solo llamar a cookies() hace que esta página sea dinámica
const cookieStore = await cookies();
const sessionToken = cookieStore.get("session")?.value;
if (!sessionToken) {
redirect("/login");
}
const user = await getUserFromSession(sessionToken);
return <Profile user={user} />;
}
Este es el patrón estándar para páginas autenticadas. Verificar una cookie de sesión fuerza el renderizado dinámico de forma natural.
Solución 4: Acceder a searchParams hace las páginas dinámicas
Referenciar parámetros de consulta de URL como ?page=2 activa automáticamente el renderizado dinámico.
// app/[locale]/blog/page.tsx
interface SearchParams {
page?: string;
category?: string;
}
export default async function BlogPage({
searchParams,
}: {
searchParams: Promise<SearchParams>;
}) {
// Acceder a searchParams hace que esta página sea automáticamente dinámica
const { page = "1", category } = await searchParams;
const posts = await getPosts({ page: Number(page), category });
return <PostList posts={posts} />;
}
Las páginas con paginación o filtrado naturalmente se vuelven dinámicas porque referencian searchParams.
Solución 5: revalidateTag para invalidación selectiva de caché
Para patrones ISR, usa caché con etiquetas para invalidar entradas específicas cuando el contenido se actualiza.
Next.js 16 recomienda la directiva use cache sobre unstable_cache, pero los proyectos sin dynamicIO habilitado pueden seguir usando la API anterior.
// app/[locale]/blog/[slug]/page.tsx
import { unstable_cache } from "next/cache";
// Caché con etiquetas (se recomienda migrar a use cache en Next.js 16)
const getPost = unstable_cache(
async (slug: string) => {
return fetchPostFromCMS(slug);
},
["post"],
{
tags: ["posts"],
revalidate: 3600, // also auto-revalidate every hour
}
);
export default async function PostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPost(slug);
return <article>{post.content}</article>;
}
// app/api/revalidate/route.ts
import { revalidateTag } from "next/cache";
import { NextRequest } from "next/server";
export async function POST(request: NextRequest) {
const { secret } = await request.json();
if (secret !== process.env.REVALIDATE_SECRET) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
revalidateTag("posts");
return Response.json({ revalidated: true });
}
Cuando un CMS actualiza contenido, llamar a /api/revalidate vía webhook limpia el caché. El siguiente visitante obtiene datos frescos.
generateStaticParams y parámetros dinámicos
Al usar generateStaticParams, controla qué pasa con las rutas no listadas usando dynamicParams.
// app/[locale]/blog/[slug]/page.tsx
import { notFound } from "next/navigation";
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({ slug: post.slug }));
}
// Por defecto: true — los slugs no listados se renderizan dinámicamente en el primer acceso
export const dynamicParams = true;
export default async function PostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) notFound();
return <article>{post.content}</article>;
}
Con dynamicParams = true (el valor predeterminado), acceder a un slug no listado activa SSR bajo demanda. El resultado se cachea para visitantes posteriores.
FAQ
¿Debería usar force-dynamic o cache: "no-store"?
Usa export const dynamic = "force-dynamic" cuando quieras que toda la página se renderice dinámicamente. Usa cache: "no-store" cuando solo necesites deshabilitar el caché en un fetch específico. En la práctica, un fetch con no-store hace que toda la página sea dinámica de todos modos, así que el resultado suele ser el mismo.
¿Por qué mi página funciona dinámicamente en dev pero se vuelve estática en producción?
next dev ejecuta cada página en cada solicitud, así que no puedes distinguir estática de dinámica en desarrollo. La única forma confiable es verificar la salida del build de producción (next build), que muestra ○/ƒ/● para cada ruta.
¿Cómo interactúan los Server Components y Client Components con SSR?
Los Client Components dentro de Server Components se pre-renderizan en el servidor y luego se hidratan en el cliente. La directiva "use client" no significa "solo navegador" — define el límite entre Server y Client Components. El data fetching para SSR debe hacerse en Server Components.
¿Cuál es la diferencia entre revalidate y revalidateTag?
revalidate es basado en tiempo — el caché se refresca automáticamente después del número especificado de segundos. revalidateTag es basado en eventos — invalidas programáticamente cachés específicos bajo demanda. Usa revalidateTag cuando quieras actualizaciones inmediatas disparadas por webhooks de CMS o eventos similares.
¿Por qué cambió el comportamiento del caché entre Next.js 14 y 15?
En Next.js 14, fetch usaba force-cache por defecto, lo que significaba que todo se cacheaba a menos que optaras por no hacerlo. Next.js 15 cambió esto a no-store, requiriendo que optes explícitamente por el caché. Si estás actualizando de 14 a 15, las páginas que dependían del caché implícito pueden sufrir regresiones de rendimiento.
¿unstable_cache todavía funciona?
Sí, pero Next.js 16 recomienda migrar a la directiva use cache. A diferencia de unstable_cache que solo cachea datos JSON, use cache puede cachear componentes y rutas completas. Los proyectos sin dynamicIO habilitado pueden seguir usando unstable_cache.
Mis páginas de generateStaticParams no se actualizan — ¿qué hago?
Las páginas generadas con generateStaticParams son estáticas por defecto y no reflejan cambios en los datos hasta el siguiente build. Para actualizarlas, establece un valor de revalidate o usa revalidatePath / revalidateTag para revalidación bajo demanda. Con dynamicParams = true, las rutas no listadas recurren a SSR.
Conclusión
Referencia rápida para problemas de SSR en App Router:
| Síntoma | Causa | Solución |
|---|---|---|
| Los datos no se actualizan | La página se genera estáticamente | force-dynamic o cache: "no-store" |
| Todos ven la misma página | Datos específicos del usuario obtenidos del lado del cliente | Usa cookies() / headers() para activar SSR |
El log de build muestra ○ | No se usan APIs dinámicas | Agrega dynamic = "force-dynamic" |
| Los datos antiguos persisten después de actualizar | force-cache establecido | Usa revalidate o revalidateTag |
Cuando "SSR debería funcionar pero no lo hace," el camino más rápido a la causa raíz es verificar los símbolos ○/ƒ/● en el log de build. A partir de ahí, la solución se vuelve obvia.
Artículos relacionados:
- Guía de SSR en Next.js: qué cambió con los Server Components
- Cómo solucionar errores de hidratación en Next.js
- Optimización de Build en Next.js: Guía Completa
- React Server Components explicados
- Minimizar "use client" en React Server Components
- Errores de despliegue en Vercel: Guía completa de soluciones