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
| Mode | When HTML generated | Data freshness | Use case |
|---|---|---|---|
| Static (SSG) | Build time | Stale until rebuild | Marketing, docs, blogs |
| ISR | Build + background revalidate | Up to N seconds old | Product pages, feeds |
| Dynamic (SSR) | Every request | Always fresh | User-specific, real-time |
| PPR | Static shell + dynamic islands | Mixed | Best of both worlds |
| Client-side | Browser | On demand | Dashboards, 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 used | Effect |
|---|---|
cookies() | Dynamic |
headers() | Dynamic |
searchParams prop | Dynamic |
fetch(..., { cache: 'no-store' }) | Dynamic |
fetch(..., { next: { revalidate: 0 } }) | Dynamic |
noStore() from next/cache | Dynamic |
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>
| Mechanism | Scope | When to use |
|---|---|---|
loading.tsx | Entire route segment | Whole-page loading skeleton |
<Suspense> | Specific component subtree | Granular / 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 }