๐Ÿณ IntuitiveFE
Login
โ† All concepts

Next.js Data Fetching Patterns

โฑ๏ธ ~5-minute bite ยท solve the sandbox to master

0%lesson
๐Ÿง’

5-Year-Old Metaphor

โ€” The physical, real-world picture. No jargon.

๐Ÿฝ๏ธ How you stock the kitchen determines how fresh the meal is.

๐Ÿฅซ

force-cache

Pantry staples โ€” buy once, use forever

Cached at build time, served from CDN instantly. Perfect for public data that rarely changes: blog posts, marketing pages, product catalogs. Zero runtime cost.

๐Ÿฅ›

revalidate: 60

Milk with an expiry date

Fresh for 60s, then auto-restocked in the background. Users always get something instantly โ€” at worst it's 60s old. ISR: static performance with eventual freshness.

๐Ÿšš

no-store

Fresh delivery every time you cook

Every request hits the API fresh. No caching at any layer. Use for user-specific, personalized, or highly volatile data: carts, dashboards, live feeds.

๐Ÿท๏ธ

tags + revalidateTag

Labeled shelves โ€” restock only what changed

Cache with tags; invalidate only the tagged entries. CMS publishes an article โ†’ webhook calls revalidateTag('posts') โ†’ only posts cache is purged. Surgical precision.

โšก

Promise.all

Order all dishes simultaneously, not one at a time

3 sequential fetches = 300ms. Promise.all = 100ms. Always parallelize independent data fetches. Next.js deduplicates identical fetch() calls within a render automatically.

Build output signals

โ—‹ StaticPre-rendered at build. CDN cached.
โ— ISRStatic + background revalidation.
ฮป DynamicServer-rendered every request.
๐ŸŽ›๏ธ

Interactive Sandbox

โ€” Move something, see it react instantly.

Fetch pattern

โ—‹ Static

Cached forever โ€” only refreshed on next build/deploy.

js
1// Server Component โ€” cached forever (until next deploy)
2async function Page() {
3 const res = await fetch("https://api.example.com/posts", {
4 cache: "force-cache", // default in Next.js 13/14 App Router
5 });
6 const posts = await res.json();
7ย 
8 return <PostList posts={posts} />;
9}
10ย 
11// Equivalent shorthand (force-cache is the default):
12const res = await fetch("https://api.example.com/posts");
13ย 
14// Build output:
15// โ—‹ /posts (Static) โ€” generated once, served from CDN forever

Staleness clock

โˆž freshNever expires. Data frozen at build time.
Gotcha: If your API returns different data per user (auth tokens, personalized content), force-cache will cache one user's data and serve it to everyone. Always use no-store for user-specific endpoints.
Insight: force-cache = pantry staples. You bought them once, they last years. Perfect for public, rarely-changing data: product catalog, blog posts, marketing content. Zero runtime cost โ€” CDN serves pre-built HTML.
Explored:๐Ÿฅซ๐Ÿฅ›๐Ÿšš๐Ÿท๏ธโšก
๐ŸŽฏ

Challenge

Which fetch option makes a route dynamically rendered on every request?

Try it
๐ŸŽฏ

Why Should I Care?

โ€” The exact interview question + the bug it kills.

Interview questions

Q: What is ISR (Incremental Static Regeneration)?

Pages are statically generated at build time but re-generated in the background after the revalidate window expires. Users always get cached HTML โ€” zero server render cost. The first visitor after the window expires still gets the stale page; the next visitor after thatgets the freshly regenerated one. This "stale-while-revalidate" pattern gives static performance with eventual freshness.

js
1// next: { revalidate: 60 } on the fetch, OR:
2export const revalidate = 60; // route segment config
3ย 
4// Build output:
5// โ— /blog (ISR) revalidate: 60
6//
7// Timeline:
8// t=0s : build โ†’ generate HTML
9// t=30s : user A โ†’ cached HTML (instant)
10// t=61s : user B โ†’ STILL cached (stale, but instant)
11// + background regeneration fires
12// t=62s : regeneration done โ†’ cache updated
13// t=63s : user C โ†’ fresh HTML (instant)

Q: When should you use on-demand revalidation with tags?

When you know exactly when data changes โ€” e.g., a CMS webhook fires when an article is published. Instead of polling every N seconds, you call revalidateTag('posts') from a Route Handler the moment new content is available. This gives you always-fresh data without constant unnecessary regeneration โ€” the cache stays warm until you explicitly bust it.

js
1// 1. Tag the fetch in your Server Component:
2const res = await fetch("https://cms.example.com/posts", {
3 next: { tags: ["posts", "homepage"] },
4});
5ย 
6// 2. Revalidation endpoint (Route Handler):
7// app/api/revalidate/route.ts
8import { revalidateTag } from "next/cache";
9export async function POST(req: Request) {
10 const { tag } = await req.json();
11 revalidateTag(tag); // only purges "posts" cache entries
12 return Response.json({ ok: true });
13}
14ย 
15// 3. CMS calls your webhook on publish:
16// POST /api/revalidate { "tag": "posts" }
17// โ†’ only /blog and pages tagged "posts" regenerate
18// โ†’ /products (tagged "products") stays cached

Q: Why does parallel fetch matter?

Sequential awaits add latency linearly: 3 fetches ร— 100ms = 300ms minimum before the page can render. Promise.all fires all fetches concurrently โ€” total time equals the slowest single fetch (~100ms). On a page with 5 independent data sources averaging 80ms each, the difference is 400ms vs 80ms. Always parallelize when there are no data dependencies between fetches.

js
1// โœ— Sequential: 80 + 100 + 90 + 110 + 70 = 450ms
2const user = await fetchUser();
3const orders = await fetchOrders();
4const prefs = await fetchPreferences();
5const notifs = await fetchNotifications();
6const friends = await fetchFriends();
7ย 
8// โœ“ Parallel: max(80, 100, 90, 110, 70) = 110ms
9const [user, orders, prefs, notifs, friends] = await Promise.all([
10 fetchUser(),
11 fetchOrders(),
12 fetchPreferences(),
13 fetchNotifications(),
14 fetchFriends(),
15]);
16ย 
17// Next.js deduplication bonus: identical fetch() URLs
18// called by two different Server Components are
19// automatically coalesced into one HTTP request.
๐Ÿ”ฌ

The Deep Dive

โ€” Spec refs, engine internals, the minutiae.

Fetch deduplication โ€” automatic memoization

Next.js extends the native fetch API with automatic request deduplication. If two Server Components in the same render tree call fetch('/api/user') with the same URL and options, only one HTTP request is sent. Both components receive the same response. This means you can write data-fetching logic in each component without worrying about duplicate API calls โ€” colocate data needs with the component that uses them.

js
1// Layout.tsx โ€” fetches user
2async function Layout({ children }: { children: React.ReactNode }) {
3 const user = await fetch("/api/user").then(r => r.json());
4 return <><Header user={user} />{children}</>;
5}
6ย 
7// Page.tsx โ€” also fetches user (same URL)
8async function Page() {
9 const user = await fetch("/api/user").then(r => r.json());
10 return <Profile user={user} />;
11}
12ย 
13// Result: only ONE HTTP GET /api/user is made.
14// Next.js deduplicates per render pass using a Request cache.
15// The cache is cleared between requests โ€” not persistent.

React.cache โ€” deduplication for non-fetch data sources

fetch()deduplication is automatic, but what about database queries, filesystem reads, or any async function that isn't fetch? React.cache wraps any function and memoizes its result per render pass โ€” same arguments, same result, one execution.

ts
1import { cache } from "react";
2import { db } from "@/lib/db";
3ย 
4// Wrapped in cache() โ€” deduplicates across the render tree
5export const getUser = cache(async (id: string) => {
6 return db.user.findUnique({ where: { id } });
7});
8ย 
9// Now safe to call from multiple Server Components:
10// Layout: await getUser(params.id) โ†’ DB query
11// Page: await getUser(params.id) โ†’ same result, no second query
12// Card: await getUser(params.id) โ†’ same result, no second query
13// All three get the same object. One DB round-trip total.
14ย 
15// Unlike fetch(), React.cache() does NOT persist across requests.
16// It is scoped to a single server render.

Route segment config โ€” route-level overrides

Instead of setting cache options on each individual fetch(), you can configure the entire route segment with a single export. These override per-fetch options.

js
1// app/blog/page.tsx
2ย 
3// Revalidate ALL fetches in this route every 60s:
4export const revalidate = 60;
5ย 
6// Force dynamic rendering for the entire route:
7export const dynamic = "force-dynamic";
8ย 
9// Force static generation (error if dynamic functions used):
10export const dynamic = "force-static";
11ย 
12// Control behaviour for unknown path params:
13export const dynamicParams = true; // (default) generate on-demand
14export const dynamicParams = false; // 404 for unknown params
15ย 
16// Specify the runtime:
17export const runtime = "edge"; // Edge Runtime (V8, global)
18export const runtime = "nodejs"; // Node.js (default)
19ย 
20// Preferred region for edge deployment:
21export const preferredRegion = "iad1"; // Washington DC

revalidateTag vs revalidatePath

revalidateTag(tag)

Invalidates all cache entries that were fetched with this tag โ€” regardless of which URL or route they appear on.

Use when: data is shared across multiple routes. A "products" tag might appear on /shop, /product/[id], and /homepage simultaneously.

revalidatePath(path)

Invalidates the cached response for a specific route path and all its fetches.

Use when: you know the exact page to update โ€” e.g., after a user updates their profile, call revalidatePath('/profile') to purge just that route.

js
1import { revalidateTag, revalidatePath } from "next/cache";
2ย 
3// After CMS publishes โ†’ purge all "posts" across all routes:
4revalidateTag("posts");
5ย 
6// After user profile update โ†’ purge only their profile page:
7revalidatePath(`/users/${userId}`);
8ย 
9// revalidatePath also accepts a "page" or "layout" type:
10revalidatePath("/blog", "layout"); // purge /blog and all children
11revalidatePath("/blog/[slug]", "page"); // purge just page, not layout
๐ŸŽค

Interview Questions

โ€” Real questions from real interviews โ€” with answers.

force-cache โ€” responses are cached indefinitely and served statically until revalidation.

The stale cached page โ€” and their request triggers background regeneration for the next visitor.

revalidateTag when data appears on multiple routes; revalidatePath when you know the exact URL.

searchParams is request-time data โ€” unknown at build time, so the page cannot be pre-rendered.

fetch() URLs are deduplicated per render pass automatically. Prisma requires React.cache() for the same effect.

๐ŸŽฎ

Memory Game

โ€” Quick quiz โ€” lock the concept in long-term memory.
1/4

What is the 'data cache' in Next.js App Router and how is it different from the 'full route cache'?