---
name: nextjs-data-fetching
description: >-
  Next.js App Router data fetching and caching: fetch cache lifetimes,
  tag-based revalidation, route segment config, and co-locating fetches with
  the components that use them. Use when adding or reviewing data fetching.
argument-hint: "[route or fetch]"
license: MIT
metadata:
  author: John Felix Lim
  version: "1.0.0"
---
# Data Fetching & Caching

Apply when adding a fetch or reviewing staleness or performance. Start by asking how often this specific data actually changes, then pick force-cache, a revalidate window, or no-store to match — never leave it as an unexamined default.

## Rules

1. **`fetch` is cached by default in Server Components** — The App Router extends `fetch` with an implicit cache: identical requests during a render, and across requests until revalidated, are deduplicated for free. Not knowing this is why teams reach for a data-fetching library to solve a problem the framework already solved.
2. **Revalidation is a decision, not a default** — Choose `force-cache` (the default), `no-store`, or a `next: { revalidate: N }` window deliberately per request. Leaving genuinely dynamic data at the default serves stale content to everyone who loads the page.
3. **Tag-based revalidation invalidates by meaning, not by guesswork** — `next: { tags: [...] }` plus `revalidateTag()` lets a mutation invalidate exactly the data it affected, instead of a time-based window that's either too short to help or too long to be correct.
4. **Route segment config sets the ceiling for the whole segment** — `export const dynamic`, `revalidate`, and `fetchCache` at the file level override every fetch inside it. A stray `dynamic = "force-dynamic"` left over from debugging silently defeats every carefully-tuned fetch below it.
5. **Co-locate the fetch with the component that needs the data** — Passing fetched data down through props from a distant ancestor recreates the prop drilling that Server Components exist to avoid. Fetch where you render — request deduplication makes this free even when several components fetch the same thing.

## Patterns

### Deliberate cache lifetime vs accidental staleness

**Avoid** — The default cache setting is silently wrong for data that changes faster than the page is rebuilt.

```tsx
// No cache option specified — defaults to force-cache. Fine for a
// blog post, wrong for a price that changes every few minutes.
async function getPrice(sku: string) {
  const res = await fetch(`https://api.example.com/price/${sku}`);
  return res.json();
}
```

**Prefer** — The lifetime matches how often the data actually changes, stated explicitly instead of inherited by accident.

```tsx
async function getPrice(sku: string) {
  const res = await fetch(`https://api.example.com/price/${sku}`, {
    next: { revalidate: 60 }, // stale after 60s, refetched in the background
  });
  return res.json();
}
```

### Tag-based invalidation instead of time-based guessing

**Avoid** — Readers see stale content for up to 5 minutes after every edit, or the interval gets tightened until the cache stops helping.

```tsx
// Revalidate every 5 minutes and hope that's often enough after an edit.
async function getPost(slug: string) {
  const res = await fetch(`/api/posts/${slug}`, { next: { revalidate: 300 } });
  return res.json();
}
```

**Prefer** — The edit that changes the data is what invalidates the cache — freshness is exact, not probabilistic.

```tsx
async function getPost(slug: string) {
  const res = await fetch(`/api/posts/${slug}`, {
    next: { tags: [`post-${slug}`] },
  });
  return res.json();
}

// In the mutation that edits the post:
("use server");
await updatePost(slug, data);
revalidateTag(`post-${slug}`);
```

### Segment config silently overriding fetch-level intent

**Avoid** — A segment-level flag silently overrides every fetch beneath it — the per-fetch tuning stops mattering and nobody notices.

```tsx
// app/products/[id]/page.tsx
export const dynamic = "force-dynamic"; // added for one debugging
                                          // session, never removed
async function getProduct(id: string) {
  // This fetch's own tuned revalidate window is now irrelevant —
  // force-dynamic re-runs everything on every request regardless.
  return fetch(`/api/products/${id}`, { next: { revalidate: 3600 } });
}
```

**Prefer** — Segment config is left alone unless the whole route genuinely needs to opt out of static rendering.

```tsx
// No segment-level override. Each fetch's own cache setting governs it.
async function getProduct(id: string) {
  return fetch(`/api/products/${id}`, { next: { revalidate: 3600 } });
}
```

## Review checklist

- [ ] Every fetch states its cache intent explicitly (`force-cache` default, `no-store`, or a `revalidate` window) rather than inheriting the default by accident.
- [ ] Data that changes on a specific user action uses tag-based revalidation, not a shortened time-based window.
- [ ] Route segment config (`dynamic`, `revalidate`, `fetchCache`) is set deliberately, with nothing left over from a debugging session.
- [ ] Fetches are co-located with the component that renders their data, not threaded down through props.
- [ ] Independent fetches within a segment run in parallel (`Promise.all`), not sequentially.
- [ ] `revalidateTag`/`revalidatePath` is called from the exact mutation that changes the underlying data.

---

From the John Felix Lim Frontend Engineering Playbook — https://github.com/JohnFelixLim