React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

static-generationdynamic-renderingssgssrgenerateStaticParams

Static vs Dynamic Rendering

Understand when Next.js renders routes at build time (static) versus request time (dynamic), and how to control it.

Recipe

Quick-reference recipe card -- copy-paste ready.

// Static by default -- no dynamic functions, no uncached fetches
export default async function AboutPage() {
  return <h1>About Us</h1>; // Built at deploy time
}
 
// Becomes dynamic automatically when using dynamic functions
import { cookies } from "next/headers";
 
export default async function DashboardPage() {
  const cookieStore = await cookies(); // This opts into dynamic rendering
  return <h1>Dashboard</h1>;
}
 
// Force static or dynamic with segment config
export const dynamic = "force-dynamic"; // Always render at request time
export const dynamic = "force-static";  // Always render at build time
 
// Pre-render dynamic routes at build time
export async function generateStaticParams() {
  const posts = await fetchAllPosts();
  return posts.map((p) => ({ slug: p.slug }));
}

When to reach for this: You need to understand why a route is static or dynamic, or you want to explicitly control the rendering behavior.

Working Example

// app/blog/[slug]/page.tsx -- Static generation with generateStaticParams
import { notFound } from "next/navigation";
 
type Post = { slug: string; title: string; content: string; date: string };
 
async function getPost(slug: string): Promise<Post | null> {
  const res = await fetch(`https://api.example.com/posts/${slug}`, {
    next: { tags: [`post-${slug}`] },
  });
  if (!res.ok) return null;
  return res.json();
}
 
// Pre-render these paths at build time
export async function generateStaticParams() {
  const res = await fetch("https://api.example.com/posts");
  const posts: Post[] = await res.json();
 
  return posts.map((post) => ({
    slug: post.slug,
  }));
}
 
// Generate metadata for each post
export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await getPost(slug);
  if (!post) return { title: "Not Found" };
  return { title: post.title };
}
 
export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await getPost(slug);
  if (!post) notFound();
 
  return (
    <article className="prose max-w-2xl mx-auto p-6">
      <h1>{post.title}</h1>
      <time className="text-gray-500">{post.date}</time>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}
// app/dashboard/page.tsx -- Dynamic rendering (uses cookies)
import { cookies } from "next/headers";
import { db } from "@/lib/db";
import { verifySession } from "@/lib/auth";
 
export default async function DashboardPage() {
  const cookieStore = await cookies();
  const token = cookieStore.get("session")?.value;
  const session = await verifySession(token);
 
  if (!session) {
    return <p>Please sign in</p>;
  }
 
  const stats = await db.userStats.findUnique({
    where: { userId: session.userId },
  });
 
  return (
    <main className="p-6">
      <h1 className="text-2xl font-bold">Welcome, {session.user.name}</h1>
      <div className="grid grid-cols-3 gap-4 mt-6">
        <StatCard label="Orders" value={stats?.orders ?? 0} />
        <StatCard label="Revenue" value={`$${stats?.revenue ?? 0}`} />
        <StatCard label="Customers" value={stats?.customers ?? 0} />
      </div>
    </main>
  );
}
 
function StatCard({ label, value }: { label: string; value: string | number }) {
  return (
    <div className="border rounded p-4">
      <p className="text-sm text-gray-500">{label}</p>
      <p className="text-2xl font-bold">{value}</p>
    </div>
  );
}

What this demonstrates:

  • A blog post page that is statically generated at build time using generateStaticParams
  • A dashboard page that is dynamically rendered because it calls cookies()
  • Metadata generation for static pages
  • The fetch result is cached and revalidated by tag

Deep Dive

How It Works

  • By default, Next.js tries to statically render every route at build time. A route remains static as long as it:
    • Does not call dynamic functions (cookies(), headers(), searchParams)
    • Does not use cache: "no-store" on any fetch
    • Does not export dynamic = "force-dynamic"
  • Dynamic functions that opt a route into dynamic rendering: cookies(), headers(), searchParams (the page prop), useSearchParams() (without Suspense), and connection().
  • generateStaticParams tells Next.js which dynamic route segments to pre-render at build time. Paths not returned by this function are either rendered on-demand (and cached) or return 404, depending on dynamicParams.
  • Incremental Static Regeneration (ISR) uses revalidate to rebuild static pages in the background after a time interval. The first request after the interval gets the stale page; subsequent requests get the fresh version.
  • Static pages are served from the CDN edge with zero server computation. Dynamic pages require a server (Node.js or Edge runtime) for each request.

Variations

Controlling behavior for paths not in generateStaticParams:

// Allow rendering of paths not pre-generated (default behavior)
export const dynamicParams = true;
 
// Return 404 for paths not pre-generated
export const dynamicParams = false;

Segment-level revalidation:

// Revalidate all pages under this layout every 60 seconds
export const revalidate = 60;
 
// Never revalidate (fully static until next deploy)
export const revalidate = false;

Mixing static and dynamic on the same page with Suspense:

import { Suspense } from "react";
import { cookies } from "next/headers";
 
// The static shell renders at build time
export default function Page() {
  return (
    <main>
      <h1>Product Page</h1> {/* Static */}
      <StaticProductInfo />  {/* Static */}
 
      <Suspense fallback={<p>Loading cart...</p>}>
        <DynamicCartPreview /> {/* Dynamic -- calls cookies() */}
      </Suspense>
    </main>
  );
}
 
async function DynamicCartPreview() {
  const cookieStore = await cookies();
  const cartId = cookieStore.get("cart-id")?.value;
  // ...
}

TypeScript Notes

// generateStaticParams return type
export async function generateStaticParams(): Promise<{ slug: string }[]> {
  // ...
}
 
// For nested dynamic routes
export async function generateStaticParams(): Promise<{
  category: string;
  slug: string;
}[]> {
  // ...
}
 
// Segment config types
export const dynamic: "auto" | "force-dynamic" | "force-static" | "error" = "auto";
export const dynamicParams: boolean = true;
export const revalidate: number | false = false;
export const runtime: "nodejs" | "edge" = "nodejs";

Gotchas

  • A single cookies() call makes the entire route dynamic -- Even if 99% of the page is static content, one dynamic function opts the whole route into dynamic rendering. Fix: Use Partial Prerendering (PPR) or isolate the dynamic part in a Suspense boundary.

  • generateStaticParams runs at build time only -- New content added after the build is not pre-rendered until the next build or unless dynamicParams is true (which renders on-demand). Fix: Keep dynamicParams: true (the default) so new paths are rendered on first request and then cached.

  • force-static with dynamic functions throws an error -- If you export dynamic = "force-static" but the page calls cookies(), the build fails. Fix: Remove the dynamic function or change to dynamic = "auto".

  • revalidate: 0 does not mean "revalidate immediately" -- It means "always dynamic." Fix: Use a positive number for ISR behavior.

  • Static pages with client-side data fetching -- A page can be statically generated but still fetch data on the client with SWR or React Query. The static HTML serves as the shell. Fix: This is a valid pattern, not a bug -- just be aware of the initial empty state.

Alternatives

ApproachUse WhenDon't Use When
Static Generation (default)Content changes infrequently, can be built at deploy timeData is user-specific or real-time
ISR (revalidate: N)Content changes periodically, you want CDN speed with freshnessData must be fresh on every single request
Dynamic RenderingData is user-specific, uses cookies or headersContent is the same for all users
Partial Prerendering (PPR)You want a static shell with dynamic holesThe entire page is either fully static or fully dynamic
Client-side fetchingYou need real-time updates after the page loadsData can be fetched entirely on the server
force-staticYou want to guarantee a route is never dynamicThe route genuinely needs request-time data

FAQs

How does Next.js decide if a route is static or dynamic?

A route is static by default. It becomes dynamic if it:

  • Calls a dynamic function (cookies(), headers(), searchParams, connection())
  • Uses cache: "no-store" on a fetch
  • Exports dynamic = "force-dynamic"
What is generateStaticParams and when does it run?
  • generateStaticParams tells Next.js which dynamic route segments to pre-render at build time.
  • It runs at build time only, not at request time.
  • Paths not returned are rendered on-demand (if dynamicParams is true) or return 404.
What is the difference between static generation and ISR?
  • Static generation: pages are built once at deploy time and never change until the next build.
  • ISR (revalidate: N): pages are rebuilt in the background after N seconds. The first request after the interval gets the stale page; subsequent requests get the fresh version.
Gotcha: A single cookies() call makes my entire page dynamic. How do I fix this?
  • Use Partial Prerendering (PPR) to keep the static shell and stream the dynamic part.
  • Alternatively, isolate the dynamic component inside a <Suspense> boundary so only that portion is dynamic.
Gotcha: What happens if I export force-static but the page calls cookies()?

The build will fail with an error. You cannot force a route to be static when it uses dynamic functions. Remove the dynamic function or change to dynamic = "auto".

What does revalidate: 0 mean?

It means always render dynamically (equivalent to force-dynamic). It does not mean "revalidate immediately." Use a positive number (e.g., revalidate: 60) for ISR behavior.

Can a statically generated page still fetch data on the client?

Yes. A page can be statically generated but still fetch data on the client using SWR or React Query. The static HTML serves as the shell, and client-side data fills in after hydration.

How do I control what happens for paths not returned by generateStaticParams?
// Allow on-demand rendering for unknown paths (default)
export const dynamicParams = true;
 
// Return 404 for unknown paths
export const dynamicParams = false;
How do I type the return value of generateStaticParams in TypeScript?
export async function generateStaticParams(): Promise<
  { slug: string }[]
> {
  const posts = await fetchAllPosts();
  return posts.map((p) => ({ slug: p.slug }));
}

For nested dynamic routes, include all segment params in the return type.

What are the valid TypeScript types for the dynamic segment config export?
export const dynamic: "auto" | "force-dynamic" | "force-static" | "error" = "auto";
export const dynamicParams: boolean = true;
export const revalidate: number | false = false;
export const runtime: "nodejs" | "edge" = "nodejs";
How can I mix static and dynamic content on the same page?

Wrap the dynamic part in a <Suspense> boundary. The static shell renders at build time, and the dynamic component (e.g., one that calls cookies()) streams in at request time.

Where are static pages served from versus dynamic pages?
  • Static pages are served from the CDN edge with zero server computation.
  • Dynamic pages require a server (Node.js or Edge runtime) for each request.