While building 32blog.com, I hit this several times: "The data is updated — why isn't it showing up?" Redeploying didn't help. Clearing caches didn't help.
Almost every time, the cause was a misunderstanding of App Router's rendering strategy. The most common pattern: "I thought I was using SSR, but Next.js silently switched to static generation."
This article walks through the typical patterns where SSR doesn't behave as expected in App Router, with fixes for each.
Understanding App Router Rendering Strategies
First, the foundations. App Router pages run under one of three strategies:
| Strategy | When | Use case |
|---|---|---|
| Static Generation (SSG) | Once at build time | Content that doesn't change |
| Dynamic Rendering (SSR) | On every request | Per-user or real-time data |
| ISR | Periodically regenerated | Content that updates occasionally |
The problem is that Next.js automatically optimizes to static generation when it thinks it can. Pages you intend to be dynamic silently become static.
Does Adding "use client" Enable SSR?
Quick myth-bust first: "use client" has nothing to do with SSR.
"use client" marks a component as a Client Component. It defines Server vs. Client Component boundaries — not "don't run this on the server."
// Common misconception: "use client" means browser-only
"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>;
}
That's client-side fetching — nothing to do with SSR. Server data isn't fetched. SEO suffers too.
For actual SSR data fetching, write an async Server Component:
// Server Component data fetching (SSR/SSG)
// No "use client" = Server Component
export default async function UserProfile() {
// Executes on the server
const user = await getUser();
return <p>{user.name}</p>;
}
Why Does Next.js Switch to Static Generation?
Next.js optimizes to static generation when all of these are true:
- No dynamic APIs used (
cookies(),headers(), etc.) searchParamsis not accessed- No cache-busting fetch options
export const dynamic = "force-dynamic"is not set
A plain Server Component with data fetching looks like "I can execute this at build time and cache it" to Next.js — so it becomes SSG automatically.
How to Check: Read the Build Log
npm run build
The build log shows each page's rendering strategy:
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
○— Static generation (SSG)λ— Dynamic rendering (SSR)●— ISR (static with revalidation)
If a page shows ○ but you expect it to be dynamic, that's your SSR problem.
Fix 1: force-dynamic for Guaranteed Dynamic Rendering
The simplest fix. Add one export to the page file.
// app/[locale]/dashboard/page.tsx
// Add this — that's all it takes to force SSR
export const dynamic = "force-dynamic";
export default async function DashboardPage() {
// Runs on every request
const data = await fetchDashboardData();
return <Dashboard data={data} />;
}
Keep in mind that force-dynamic means server processing runs on every request, which increases load. Use it only where truly needed.
Fix 2: Explicitly Disable fetch Caching
Next.js 15+ defaults to no-store, so this is less common now. But if force-cache ends up in your code, it overrides the default.
// Intended as SSR, but force-cache bakes it in at build time
export default async function PricePage() {
const prices = await fetch("https://api.example.com/prices", {
cache: "force-cache", // Cached at build time → never updates
}).then(r => r.json());
return <PriceTable prices={prices} />;
}
// Always fetch fresh data
export default async function PricePage() {
const prices = await fetch("https://api.example.com/prices", {
cache: "no-store", // No cache → fetched fresh every request
}).then(r => r.json());
return <PriceTable prices={prices} />;
}
Setting no-store on any fetch automatically makes the containing page dynamic. No need to separately set force-dynamic.
Fix 3: Use Dynamic APIs to Trigger SSR
Calling cookies() or headers() tells Next.js that the page must be rendered dynamically.
// app/[locale]/profile/page.tsx
import { cookies } from "next/headers";
export default async function ProfilePage() {
// Just calling cookies() makes this page dynamic
const cookieStore = await cookies();
const sessionToken = cookieStore.get("session")?.value;
if (!sessionToken) {
redirect("/login");
}
const user = await getUserFromSession(sessionToken);
return <Profile user={user} />;
}
This is the standard pattern for authenticated pages. Checking a session cookie forces dynamic rendering naturally.
Fix 4: Accessing searchParams Makes Pages Dynamic
Referencing URL query parameters like ?page=2 automatically triggers dynamic rendering.
// app/[locale]/blog/page.tsx
interface SearchParams {
page?: string;
category?: string;
}
export default async function BlogPage({
searchParams,
}: {
searchParams: Promise<SearchParams>;
}) {
// Accessing searchParams makes this page automatically dynamic
const { page = "1", category } = await searchParams;
const posts = await getPosts({ page: Number(page), category });
return <PostList posts={posts} />;
}
Pages with pagination or filtering naturally become dynamic because they reference searchParams.
Fix 5: revalidateTag for Targeted Cache Invalidation
For ISR patterns, use tagged caching to bust specific cache entries when content updates.
// 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 });
}
When a CMS updates content, hitting /api/revalidate via webhook clears the cache. The next visitor gets fresh data.
generateStaticParams and Dynamic Params
When using generateStaticParams, control what happens for unlisted paths using dynamicParams.
// app/[locale]/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({ slug: post.slug }));
}
// Default: true — unlisted slugs are rendered dynamically on first access
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>;
}
With dynamicParams = true (the default), accessing an unlisted slug triggers on-demand SSR. The result is cached for subsequent visitors.
Wrapping Up
Quick reference for SSR issues in App Router:
| Symptom | Cause | Fix |
|---|---|---|
| Data not updating | Page is statically generated | force-dynamic or cache: "no-store" |
| Everyone sees the same page | User-specific data fetched client-side | Use cookies() / headers() to trigger SSR |
Build log shows ○ | No dynamic APIs used | Add dynamic = "force-dynamic" |
| Old data persists after update | force-cache set | Use revalidate or revalidateTag |
When "SSR should be working but isn't," the fastest path to root cause is checking the build log's ○/λ/● symbols. From there, the fix becomes obvious.