Next.js Cheatsheet

Rendering (SSR, SSG, ISR)

Use this Next.js reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

Rendering Modes Overview

ModeWhen HTML generatedData freshnessUse case
Static (SSG)Build timeStale until rebuildMarketing, docs, blogs
ISRBuild + background revalidateUp to N seconds oldProduct pages, feeds
Dynamic (SSR)Every requestAlways freshUser-specific, real-time
PPRStatic shell + dynamic islandsMixedBest of both worlds
Client-sideBrowserOn demandDashboards, live data

Static Rendering (Default)

Routes with no dynamic data are statically generated at build time automatically.

// app/about/page.tsx — no dynamic data → static HTML at build
export default function About() {
  return <h1>About Us</h1>
}
// fetch with force-cache → treated as static
export default async function Page() {
  const data = await fetch('https://api.example.com/data', {
    cache: 'force-cache',
  }).then(r => r.json())

  return <div>{data.title}</div>
}

Dynamic Rendering (SSR)

A route becomes dynamic when it uses request-time information. Next.js detects this automatically.

Triggers for dynamic rendering:

API usedEffect
cookies()Dynamic
headers()Dynamic
searchParams propDynamic
fetch(..., { cache: 'no-store' })Dynamic
fetch(..., { next: { revalidate: 0 } })Dynamic
noStore() from next/cacheDynamic
import { cookies, headers } from 'next/headers'

export default async function Dashboard() {
  const cookieStore = await cookies()          // → dynamic
  const user = await getUser(cookieStore.get('session')?.value)
  return <div>Hello {user.name}</div>
}

Force Dynamic or Static

// Force a route to always be dynamic (SSR every request)
export const dynamic = 'force-dynamic'

// Force a route to be static (errors if dynamic APIs are used)
export const dynamic = 'error'

// Force static even if dynamic APIs detected (use carefully)
export const dynamic = 'force-static'

Incremental Static Regeneration (ISR)

Revalidate static pages in the background after a time period.

Time-based revalidation:

// app/products/page.tsx
export const revalidate = 60   // revalidate at most every 60 seconds

export default async function Products() {
  const products = await fetch('https://api.example.com/products', {
    next: { revalidate: 60 },
  }).then(r => r.json())

  return <ul>{products.map((p: Product) => <li key={p.id}>{p.name}</li>)}</ul>
}

On-demand revalidation (triggered by a Route Handler or Server Action):

// app/api/revalidate/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { revalidatePath, revalidateTag } from 'next/cache'

export async function POST(request: NextRequest) {
  const { path, tag, secret } = await request.json()

  if (secret !== process.env.REVALIDATION_SECRET) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  if (path) revalidatePath(path)
  if (tag) revalidateTag(tag)

  return NextResponse.json({ revalidated: true, now: Date.now() })
}
// Tag a fetch so it can be invalidated
const posts = await fetch('https://api.example.com/posts', {
  next: { tags: ['posts'] },
}).then(r => r.json())

// Invalidate from an action
revalidateTag('posts')

generateStaticParams — SSG for Dynamic Routes

Pre-generate static pages for dynamic segments at build time.

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then(r => r.json())

  return posts.map((post: Post) => ({ slug: post.slug }))
  // returns: [{ slug: 'hello-world' }, { slug: 'next-js-guide' }, ...]
}

export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const post = await fetchPost(slug)
  return <article>{post.body}</article>
}

With catch-all routes:

// app/docs/[...slug]/page.tsx
export async function generateStaticParams() {
  return [
    { slug: ['getting-started'] },
    { slug: ['api', 'reference'] },
  ]
}

dynamicParams — control what happens for paths NOT in generateStaticParams:

export const dynamicParams = true    // (default) render on demand and cache
export const dynamicParams = false   // 404 for any path not pre-generated

Partial Pre-rendering (PPR) — Next 15 Experimental

Static shell served immediately; dynamic holes streamed in. No separate ISR route needed. In Next 16 the experimental ppr flag is replaced by cacheComponents: true (see the Env and Config sheet); the flag below is Next 15 only.

// next.config.ts (Next 15)
export default {
  experimental: { ppr: 'incremental' }
}
// app/page.tsx
import { Suspense } from 'react'
import { StaticHero } from '@/components/static-hero'   // static
import { DynamicFeed } from '@/components/dynamic-feed' // dynamic

export const experimental_ppr = true

export default function Home() {
  return (
    <>
      <StaticHero />                         {/* baked into static shell */}
      <Suspense fallback={<FeedSkeleton />}>
        <DynamicFeed />                      {/* streamed after */}
      </Suspense>
    </>
  )
}

Streaming and Suspense

Streaming sends HTML progressively — critical content appears before slow data loads.

// app/dashboard/page.tsx
import { Suspense } from 'react'

export default function Dashboard() {
  return (
    <main>
      <h1>Dashboard</h1>  {/* immediate */}

      <Suspense fallback={<RevenueCardSkeleton />}>
        <RevenueCard />    {/* streams when ready */}
      </Suspense>

      <Suspense fallback={<TableSkeleton />}>
        <RecentOrders />   {/* streams independently */}
      </Suspense>
    </main>
  )
}

loading.tsx vs <Suspense>

MechanismScopeWhen to use
loading.tsxEntire route segmentWhole-page loading skeleton
<Suspense>Specific component subtreeGranular / parallel streaming

Segment Config Reference

// At the top of any page.tsx, layout.tsx, or route.ts

export const dynamic = 'auto'          // (default) infer from usage
export const dynamic = 'force-dynamic' // SSR on every request
export const dynamic = 'force-static'  // static; dynamic APIs return defaults
export const dynamic = 'error'         // static; throw if dynamic APIs used

export const revalidate = false        // cache forever (default for static)
export const revalidate = 0            // no cache (dynamic)
export const revalidate = 60           // revalidate every 60s (ISR)

export const fetchCache = 'auto'       // default Next.js behavior
export const fetchCache = 'only-cache' // all fetches must be cacheable
export const fetchCache = 'force-cache'
export const fetchCache = 'force-no-store'

export const runtime = 'nodejs'        // (default) full Node.js APIs
export const runtime = 'edge'          // Edge Runtime (faster cold start)

export const preferredRegion = 'auto'
export const preferredRegion = 'home'  // deploy to closest region to DB

export const maxDuration = 60          // max serverless execution seconds

noStore() — Opt Out of Caching

import { unstable_noStore as noStore } from 'next/cache'

export async function getLatestInvoices() {
  noStore()   // equivalent to cache: 'no-store' on the enclosing fetch
  const data = await sql`SELECT * FROM invoices ORDER BY date DESC LIMIT 5`
  return data.rows
}