Skip to main content
Technology & EngineeringNextjs272 lines

Data Fetching

Data fetching and caching strategies including Server Components, fetch options, ISR, and the Next.js cache layers

Quick Summary27 lines
You are an expert in Next.js data fetching patterns, caching strategies, and revalidation for building fast, up-to-date applications.

## Key Points

1. **Request Memoization**: Identical `fetch` calls within a single render are automatically deduplicated by React.
2. **Data Cache**: Server-side cache that persists `fetch` results across requests and deployments.
3. **Full Route Cache**: Pre-rendered HTML and RSC payload cached at build time for static routes.
4. **Router Cache**: Client-side in-memory cache of visited route segments (prefetched and navigated).
- Fetch data in Server Components by default — no client bundle cost, no loading spinners for initial render.
- Use `Promise.all` to parallelize independent fetches and avoid sequential waterfalls.
- Wrap slow data dependencies in `<Suspense>` for progressive streaming.
- Use `revalidateTag` for surgical cache invalidation; prefer it over `revalidatePath` when possible.
- Leverage React's built-in `cache()` function to deduplicate identical calls across the component tree.
- Set `revalidate` at the most specific level (per-fetch rather than per-route) for fine-grained control.
- **Waterfall fetches**: Sequential `await` calls in a single component create request waterfalls. Use `Promise.all` or move fetches into separate `<Suspense>`-wrapped components.
- **Caching surprises**: In development (`next dev`), fetch caching is disabled by default. Behavior differs in production builds.

## Quick Example

```tsx
// app/dashboard/page.tsx
export const dynamic = "force-dynamic";      // Always SSR
// export const dynamic = "force-static";    // Always SSG
// export const revalidate = 3600;           // ISR for the whole route
```
skilldb get nextjs-skills/Data FetchingFull skill: 272 lines
Paste into your CLAUDE.md or agent config

Data Fetching — Next.js

You are an expert in Next.js data fetching patterns, caching strategies, and revalidation for building fast, up-to-date applications.

Overview

Next.js App Router embraces React Server Components as the primary data-fetching layer. You fetch data directly in async Server Components using fetch, database clients, or any async API. Next.js extends the native fetch with caching and revalidation options and provides multiple cache layers that can be configured per-request.

Core Philosophy

Data fetching in the App Router is built on a simple premise: fetch where you render. Instead of centralizing data requirements in getServerSideProps or getStaticProps at the page level, each Server Component fetches exactly the data it needs. This co-location eliminates prop drilling, reduces over-fetching, and makes components self-contained and portable.

The caching model is aggressive by default and opt-out by design. Next.js caches fetch responses, memoizes identical calls within a render, and pre-renders static routes at build time. This means your application is fast out of the box, but you must consciously opt out of caching when freshness matters. Understanding the four cache layers — request memoization, data cache, full route cache, and router cache — is essential to avoiding stale data bugs that only appear in production.

Revalidation is the bridge between static performance and dynamic freshness. Rather than choosing between "fully static" and "fully dynamic," Next.js lets you set time-based revalidation windows or trigger on-demand revalidation after mutations. The goal is to serve cached content whenever possible and refresh it precisely when the underlying data changes, giving users both speed and accuracy without forcing a binary architectural decision.

Core Concepts

Fetching in Server Components

// app/products/page.tsx
export default async function ProductsPage() {
  const res = await fetch("https://api.example.com/products", {
    next: { revalidate: 3600 }, // ISR: revalidate every hour
  });
  const products = await res.json();

  return (
    <ul>
      {products.map((p: Product) => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  );
}

Fetch Caching Options

// Static: cached indefinitely until manually revalidated (default in production)
fetch(url, { cache: "force-cache" });

// ISR: cache with time-based revalidation
fetch(url, { next: { revalidate: 60 } });

// Dynamic: no cache, fresh on every request
fetch(url, { cache: "no-store" });

// Tag-based revalidation
fetch(url, { next: { tags: ["products"] } });

The Four Cache Layers

  1. Request Memoization: Identical fetch calls within a single render are automatically deduplicated by React.
  2. Data Cache: Server-side cache that persists fetch results across requests and deployments.
  3. Full Route Cache: Pre-rendered HTML and RSC payload cached at build time for static routes.
  4. Router Cache: Client-side in-memory cache of visited route segments (prefetched and navigated).

Route Segment Config

Force an entire route to be dynamic or static:

// app/dashboard/page.tsx
export const dynamic = "force-dynamic";      // Always SSR
// export const dynamic = "force-static";    // Always SSG
// export const revalidate = 3600;           // ISR for the whole route

Implementation Patterns

Parallel Data Fetching

Avoid sequential waterfalls by fetching in parallel:

export default async function Dashboard() {
  // Start all fetches simultaneously
  const [userData, ordersData, analyticsData] = await Promise.all([
    getUser(),
    getOrders(),
    getAnalytics(),
  ]);

  return (
    <div>
      <UserProfile user={userData} />
      <OrderList orders={ordersData} />
      <AnalyticsChart data={analyticsData} />
    </div>
  );
}

Streaming with Suspense

For independent sections that can load progressively:

import { Suspense } from "react";

export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<Skeleton />}>
        <SlowComponent />
      </Suspense>
      <Suspense fallback={<Skeleton />}>
        <AnotherSlowComponent />
      </Suspense>
    </div>
  );
}

// Each component fetches its own data
async function SlowComponent() {
  const data = await fetchSlowData(); // Can take 2+ seconds
  return <div>{/* render data */}</div>;
}

Database Queries with Caching

Use unstable_cache (or the newer "use cache" directive when stable) for non-fetch data sources:

import { unstable_cache } from "next/cache";
import { db } from "@/lib/db";

const getProducts = unstable_cache(
  async (category: string) => {
    return db.product.findMany({
      where: { category },
      orderBy: { createdAt: "desc" },
    });
  },
  ["products"],               // cache key parts
  { revalidate: 3600, tags: ["products"] }
);

export default async function ProductsPage({
  searchParams,
}: {
  searchParams: Promise<{ category?: string }>;
}) {
  const { category } = await searchParams;
  const products = await getProducts(category ?? "all");
  return <ProductGrid products={products} />;
}

On-Demand Revalidation

Trigger revalidation from Server Actions or Route Handlers:

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

export async function publishPost(id: string) {
  await db.post.update({ where: { id }, data: { published: true } });

  // Revalidate by tag (preferred — surgical)
  revalidateTag("posts");

  // Or by path
  revalidatePath("/blog");
}

Client-Side Fetching (when needed)

For user-specific, frequently-updating, or interactive data, fetch on the client:

"use client";

import useSWR from "swr";

const fetcher = (url: string) => fetch(url).then((r) => r.json());

export function Notifications() {
  const { data, error, isLoading } = useSWR("/api/notifications", fetcher, {
    refreshInterval: 30_000, // Poll every 30 seconds
  });

  if (isLoading) return <Spinner />;
  if (error) return <p>Failed to load</p>;

  return (
    <ul>
      {data.map((n: Notification) => (
        <li key={n.id}>{n.message}</li>
      ))}
    </ul>
  );
}

Preloading Data

Start fetching early so the data is ready when the component renders:

// lib/data.ts
import { cache } from "react";

export const getUser = cache(async (id: string) => {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
});

export const preloadUser = (id: string) => {
  void getUser(id);
};
// app/user/[id]/page.tsx
import { getUser, preloadUser } from "@/lib/data";

export default async function UserPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  preloadUser(id); // Start fetch immediately
  // ... other work ...
  const user = await getUser(id); // Resolves instantly if already fetched
  return <UserProfile user={user} />;
}

Best Practices

  • Fetch data in Server Components by default — no client bundle cost, no loading spinners for initial render.
  • Use Promise.all to parallelize independent fetches and avoid sequential waterfalls.
  • Wrap slow data dependencies in <Suspense> for progressive streaming.
  • Use revalidateTag for surgical cache invalidation; prefer it over revalidatePath when possible.
  • Leverage React's built-in cache() function to deduplicate identical calls across the component tree.
  • Set revalidate at the most specific level (per-fetch rather than per-route) for fine-grained control.

Common Pitfalls

  • Waterfall fetches: Sequential await calls in a single component create request waterfalls. Use Promise.all or move fetches into separate <Suspense>-wrapped components.
  • Caching surprises: In development (next dev), fetch caching is disabled by default. Behavior differs in production builds.
  • Stale Router Cache: The client-side Router Cache can serve stale data after a mutation. Call router.refresh() or use revalidatePath/revalidateTag in Server Actions to bust it.
  • Dynamic by accident: Using cookies(), headers(), or searchParams makes a route dynamic. If you need static generation, restructure to avoid these at the page level.
  • Ignoring error handling: A failed fetch in a Server Component crashes the render. Always check res.ok or wrap in try/catch.

Anti-Patterns

  • Fetching in a parent and prop-drilling to children. The App Router is designed for co-located data fetching. When a parent fetches data just to pass it down three levels, you create tight coupling, over-fetching, and waterfall delays. Let each component fetch its own data — React's request memoization ensures duplicate calls are deduplicated automatically.

  • Using cache: "no-store" on every fetch "just to be safe." This disables all caching and forces every request to hit the origin server, negating one of the biggest performance advantages of Next.js. Default to cached fetches and opt out selectively with revalidate intervals or no-store only when the data is truly user-specific or time-critical.

  • Calling revalidatePath("/", "layout") after every mutation. This nuclear option purges the entire cache, defeating the purpose of incremental revalidation. Use revalidateTag for surgical invalidation of specific data, or revalidatePath scoped to the exact route that changed.

  • Mixing client-side SWR/React Query for data that Server Components could fetch. Client-side fetching adds JavaScript bundle weight, loading spinners, and layout shift. Reserve SWR and React Query for genuinely interactive, user-specific, or real-time data. For initial page loads and static content, Server Components are faster and simpler.

  • Ignoring the difference between development and production caching. next dev disables fetch caching by default, so everything appears fresh. In production, the same code may serve aggressively cached responses. Always test caching behavior with next build && next start before deploying.

Install this skill directly: skilldb add nextjs-skills

Get CLI access →