32blogby StudioMitsu
nextjs7 min read

Why SSR Isn't Working in Next.js App Router (And How to Fix It)

Data not updating, pages cached when they shouldn't be — common App Router SSR issues explained with concrete fixes.

nextjsSSRApp Routercachingdynamic renderingdebugging
On this page

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:

StrategyWhenUse case
Static Generation (SSG)Once at build timeContent that doesn't change
Dynamic Rendering (SSR)On every requestPer-user or real-time data
ISRPeriodically regeneratedContent 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."

tsx
// 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:

tsx
// 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:

  1. No dynamic APIs used (cookies(), headers(), etc.)
  2. searchParams is not accessed
  3. No cache-busting fetch options
  4. 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

bash
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.

tsx
// 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.

tsx
// 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} />;
}
tsx
// 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.

tsx
// 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.

tsx
// 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.

tsx
// 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>;
}
tsx
// 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.

tsx
// 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:

SymptomCauseFix
Data not updatingPage is statically generatedforce-dynamic or cache: "no-store"
Everyone sees the same pageUser-specific data fetched client-sideUse cookies() / headers() to trigger SSR
Build log shows No dynamic APIs usedAdd dynamic = "force-dynamic"
Old data persists after updateforce-cache setUse 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.