Data Fetching
Data fetching and caching strategies including Server Components, fetch options, ISR, and the Next.js cache layers
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 linesData 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
- Request Memoization: Identical
fetchcalls within a single render are automatically deduplicated by React. - Data Cache: Server-side cache that persists
fetchresults across requests and deployments. - Full Route Cache: Pre-rendered HTML and RSC payload cached at build time for static routes.
- 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.allto parallelize independent fetches and avoid sequential waterfalls. - Wrap slow data dependencies in
<Suspense>for progressive streaming. - Use
revalidateTagfor surgical cache invalidation; prefer it overrevalidatePathwhen possible. - Leverage React's built-in
cache()function to deduplicate identical calls across the component tree. - Set
revalidateat the most specific level (per-fetch rather than per-route) for fine-grained control.
Common Pitfalls
- Waterfall fetches: Sequential
awaitcalls in a single component create request waterfalls. UsePromise.allor 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 userevalidatePath/revalidateTagin Server Actions to bust it. - Dynamic by accident: Using
cookies(),headers(), orsearchParamsmakes a route dynamic. If you need static generation, restructure to avoid these at the page level. - Ignoring error handling: A failed
fetchin a Server Component crashes the render. Always checkres.okor 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 withrevalidateintervals orno-storeonly 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. UserevalidateTagfor surgical invalidation of specific data, orrevalidatePathscoped 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 devdisables fetch caching by default, so everything appears fresh. In production, the same code may serve aggressively cached responses. Always test caching behavior withnext build && next startbefore deploying.
Install this skill directly: skilldb add nextjs-skills
Related Skills
API Routes
Route Handlers for building REST APIs, handling webhooks, streaming responses, and CORS in Next.js App Router
App Router
App Router fundamentals including file-based routing, layouts, loading states, and parallel routes in Next.js
Authentication
Authentication patterns using NextAuth.js (Auth.js) and Clerk for protecting routes and managing sessions in Next.js
Deployment
Deployment strategies for Next.js including Vercel, self-hosting with Node.js, Docker containers, and static export
Image Optimization
Image optimization with next/image, asset handling, fonts, and performance tuning for Next.js applications
Middleware
Middleware patterns for request interception, redirects, rewrites, authentication guards, and geo-routing in Next.js