Handbooks / Next.js / Chapter 3
Data Fetching and Caching
7 pages · ~5 min✓ Reviewed
Builds on Server and Client Components. Next up: Navigation and the Client Router Cache.
Data Fetching Mental Model
1. Where Fetching Runs
await fetch(url)in an async server component runs on the server- Only rendered output ships: no client JS for the fetch
- Fetch where the data is used; the same URL is deduplicated per render
- Secrets stay on the server; never pass them as props to client components
| Source | Deduped? | Fix |
|---|---|---|
fetch(url) | Yes, one render pass | none |
| DB / ORM call | No | wrap in cache() |
import { cache } from 'react' export const getUser = cache( (id) => db.user.findUnique({ where: { id } }) )
direct DB call, no API route
2. Avoid Waterfalls
- Sequential awaits make a waterfall: each waits for the last
- Independent requests: start together with
Promise.all
const [user, posts] = await Promise.all([ getUser(id), getPosts(id), ])
Static vs Dynamic Rendering
1. Two Modes
- Static: HTML built at build time (or on revalidate), served from cache
- Dynamic: HTML built per request
- Static: fastest, CDN-cacheable, can go stale
- Dynamic: always fresh, costs server time per request
2. What Makes a Route Dynamic
cookies()andheaders()searchParamsprop- An uncached fetch under your version's caching defaults
export const dynamic = 'force-dynamic' // or 'force-static'
route segment config forces a mode
3. Check the Build
- ○ in build output = static
- ƒ in build output = dynamic
One request-time API in a layout makes every child route dynamic.
Revalidation: Time-Based and On-Demand
1. Time-Based
fetch(url, { next: { revalidate: 60 } })per fetchexport const revalidate = 60per route- Stale-while-revalidate: cached data is served, then refreshed in the background
- Requestwithin 60s
- Cachedserved at once
- After 60sstill served stale
- Refreshbackground
2. On-Demand
revalidatePath('/blog')invalidates that route's cache- Call it from a server action or route handler
- Tag the fetch, then
revalidateTag('posts')hits every fetch with that tag
fetch(url, { next: { tags: ['posts'] } })
// after the mutation succeeds
await db.post.create(data)
revalidateTag('posts')revalidate after the write, not before
The Next.js Caches
1. The Four Caches
| Cache | Holds | Lifetime |
|---|---|---|
| Request Memoization | identical fetch calls in one render | one request |
| Data Cache | fetch results on the server | across requests and deploys |
| Full Route Cache | HTML + RSC payload of static routes | built at build time |
| Router Cache | visited route segments in the browser | one session |
2. Control and Invalidation
- Data Cache:
revalidate, tags,cache: 'force-cache'or'no-store' - Full Route Cache: cleared when the Data Cache revalidates, or on redeploy
- Router Cache: back and forward are instant;
router.refresh()clears it
Streaming, Suspense and generateStaticParams
1. Streaming and Suspense
- Shellsent first
- Fallbackskeleton shows
- Slow partstreams in later
- Streaming sends HTML in chunks: better time to first byte, faster feel
- Wrap a slow async server component in
<Suspense> - Only that subtree waits; the rest renders at once
loading.tsx= automatic Suspense around the page, instant on navigation
<Header />
<Suspense fallback={<Skeleton />}>
<SlowFeed />
</Suspense>SlowFeed is an async server component
2. generateStaticParams
- Returns an array of params: pre-renders
/blog/[slug]at build time - List only the top pages; the rest generate on first visit
export async function generateStaticParams() { return [{ slug: 'a' }, { slug: 'b' }] } export const dynamicParams = false
| Unlisted param |
|---|
Common Mistakes and Cheat Sheet
1. Production Mistakes
- Client
useEffectfetch: adds a waterfall and ships extra JS; use a server component - Sequential
awaits: independent requests queue up instead of running together cookies()/headers()high in the tree: the whole route silently turns dynamic- Revalidating before the mutation ends, or fetches without
tags: stale data stays
const [user, posts] = await Promise.all([ getUser(), getPosts(), ])
start together, wait together
await db.post.create(data) revalidateTag('posts')
revalidate after the mutation; the fetch needs next: { tags: ['posts'] }
2. Cheat Sheet
| Tool | Effect |
|---|---|
revalidate = N | timed refresh |
revalidatePath / revalidateTag | on demand refresh |
dynamic = 'force-dynamic' | render per request |
Check yourself
A root layout reads cookies() for a theme. Your blog page sets revalidate = 60. What does the build show?
- Every child route is ƒ dynamic, rendered per request
- One request-time API in a layout makes the whole subtree dynamic
- Fix: read the cookie in a small component lower down
Each fetch takes 1s. About how long does this page take, and how do you fix it?
- About 2s: the second await waits for the first, a waterfall
- Run them together:
await Promise.all([getUser(), getPosts()]) - That takes about 1s, the slowest request
const a = await getUser() const b = await getPosts()
A server action calls revalidateTag('posts'), but the list never updates. Why?
- The list fetch carries no tag, so nothing matches
- Add
next: { tags: ['posts'] }to that fetch - Call revalidation after the mutation finishes