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.
// app/[locale]/blog/[slug]/page.tsx
import { unstable_cache } from "next/cache";
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.
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.