intermediate11 min readPart 2 of 3

Next.js Data Fetching & Caching: fetch, ISR, and Revalidation

Understand how the Next.js App Router really fetches and caches: fetch defaults, static vs dynamic rendering, ISR with revalidate, generateStaticParams, and tag-based revalidation.

TechLogHub
·

Prerequisites

  • •Part 1 of this series (App Router basics)
  • •Familiarity with async/await and HTTP

What You'll Learn

  • Fetch data in server components with correct cache options
  • Distinguish static, dynamic, and ISR rendering — and control each
  • Pre-render dynamic routes with generateStaticParams
  • Invalidate cached data on demand with revalidatePath and revalidateTag

Data Belongs in Server Components

The App Router's data story starts simply: fetch where you render.

export default async function ReleasesPage() {
  const res = await fetch("https://api.github.com/repos/vercel/next.js/releases");
  const releases = await res.json();
  return <ReleaseList releases={releases} />;
}

No client-side waterfalls, no exposed API keys, no loading spinners for first paint. The interesting question is when this code runs — and that's what caching controls.

fetch and Its Cache Options

In current Next.js, fetch in server components is not cached by default — each request hits the network unless you opt in:

// Cache indefinitely (until manually revalidated) — static data
await fetch(url, { cache: "force-cache" });

// Cache, but refresh in the background at most every hour — ISR per-request await fetch(url, { next: { revalidate: 3600 } });

// Never cache — always fresh await fetch(url, { cache: "no-store" });

// Cache and label it for targeted invalidation await fetch(url, { next: { tags: ["releases"] } });

For non-fetch data sources (database clients, SDKs), the same ideas apply at the route level, or via React's cache() for request-level deduplication.

The Three Rendering Modes

Every route ends up in one of three modes:

Static (build time)

If a route uses no dynamic APIs (cookies(), headers(), searchParams) and no uncached data, Next.js renders it once at build and serves pure HTML from then on. Fastest possible response; content frozen until the next deploy.

Dynamic (request time)

Touch a dynamic API or uncached data, and the route renders per request. Personalization and always-fresh data — at per-request server cost. You can force it explicitly:

export const dynamic = "force-dynamic";

ISR — the Middle Path

Incremental Static Regeneration serves static HTML and keeps it fresh. One line opts a whole route in:

export const revalidate = 300; // regenerate in background at most every 5 min

The behavior to internalize: after 300 seconds, the next visitor still gets the old page instantly while regeneration runs in the background; the visitor after that gets the fresh one. Stale-while-revalidate, built in. This is the sweet spot for content sites: product listings, blog posts, docs — static speed, hourly-or-better freshness, and it degrades gracefully if a regeneration fails (the last good page keeps serving).

Pre-rendering Dynamic Routes

Dynamic segments render on demand by default. generateStaticParams tells Next.js which ones to build ahead of time:

// app/blog/[slug]/page.tsx
export const revalidate = 3600;

export async function generateStaticParams() { const posts = await getAllPosts(); return posts.map((p) => ({ slug: p.slug })); }

Known posts become static pages at build; unknown slugs render on first request and are then cached — the classic pattern for blogs and catalogs.

On-Demand Revalidation

Time-based freshness is a guess. When you know data changed — a CMS webhook fired, an admin hit save — invalidate precisely:

"use server";
import { revalidatePath, revalidateTag } from "next/cache";

export async function publishPost(slug: string) { await db.post.publish(slug); revalidatePath("/blog/" + slug); // this page revalidateTag("posts"); // every fetch tagged "posts" }

Tags are the scalable version: tag your fetches by entity (posts, product:42), then invalidate the tag wherever mutations happen. The next request re-fetches only what was tagged.

A Decision Cheat Sheet

  • Marketing page, docs → static (default, no config)
  • Blog / product pages → ISR: revalidate + generateStaticParams
  • Dashboard, personalized feed → dynamic (cookies() makes it automatic)
  • After any mutation → revalidateTag / revalidatePath
  • Truly real-time data → dynamic route + cache: "no-store" fetches
  • Debugging Cache Behavior

    Two habits save hours: run next build and read the route table — it marks each route static, ISR (revalidate column), or dynamic — and when a page "won't update", check in order: fetch cache option → route revalidate → whether a dynamic API silently made everything dynamic.


    Next: writing data back — Server Actions, forms, and optimistic UI — in Part 3: Server Actions & Forms.

    nextjscachingisrdata-fetchingreact

    Keep the references coming

    One email a week: new and trending developer tools, fresh comparisons, and what shipped. Unsubscribe in one click.