Next.js Cheatsheet

Rendering (SSR, SSG, ISR)

Use this Next.js reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

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
}