Data Fetching and Caching

intermediate
12 min

Data Fetching and Caching

Data fetching in the App Router is refreshingly direct: make your Server Component async and await your data. The subtle part is caching - deciding whether data is fetched once at build time, on every request, or somewhere in between. This lesson gives you the patterns and the mental model.

Fetching in Server Components

No useEffect, no loading state juggling, no client-side waterfalls. Just await:

jsx
// app/products/page.js export default async function ProductsPage() { const res = await fetch('https://api.example.com/products'); if (!res.ok) throw new Error('Failed to fetch products'); const products = await res.json(); return ( <ul> {products.map((p) => <li key={p.id}>{p.name} - ${p.price}</li>)} </ul> ); }

You can also skip HTTP entirely and query a database or ORM directly inside the component - it runs only on the server.

The Three Caching Strategies

Next.js extends fetch with caching options that map to three strategies:

jsx
// 1. Static - fetch once, reuse the result (like getStaticProps) fetch(url, { cache: 'force-cache' }); // 2. Dynamic - fetch on every request (like getServerSideProps) fetch(url, { cache: 'no-store' }); // 3. Incremental Static Regeneration - revalidate on a timer fetch(url, { next: { revalidate: 60 } }); // fresh at most every 60s

With revalidate, the first visitor after the window expires still gets the cached page instantly while Next.js regenerates it in the background. This gives static-site speed with data that stays reasonably fresh - ideal for blogs, product listings, and marketing pages.

A version note: in Next.js 14, fetch results were cached by default; since Next.js 15 they are uncached by default. Do not rely on the default - state your intent explicitly with cache or revalidate options.

Route-Level Configuration

You can also configure caching per route segment with exported constants:

jsx
// app/news/page.js export const revalidate = 300; // revalidate this page every 5 minutes export const dynamic = 'force-dynamic'; // or: force this page to render per request

Segment options apply to the whole page, which is handy when data comes from a database rather than fetch.

Request Deduplication

Worried about calling the same fetch in a layout and a page? Do not be. Next.js deduplicates identical fetch requests within a single render pass, so the network call happens once. For non-fetch data sources, wrap the function in React's cache():

jsx
import { cache } from 'react'; import { db } from '@/lib/db'; export const getUser = cache(async (id) => { return db.user.findUnique({ where: { id } }); });

Streaming with loading.js and Suspense

Slow data should not block the whole page. Drop a loading.js next to a page and Next.js instantly shows it while the page's data loads:

jsx
// app/products/loading.js export default function Loading() { return <p>Loading products...</p>; }

For finer control, wrap slow components in Suspense so the rest of the page streams in immediately:

jsx
import { Suspense } from 'react'; <Suspense fallback={<p>Loading reviews...</p>}> <Reviews productId={id} /> </Suspense>

On-Demand Revalidation

When content changes because of an action - say, publishing a post - refresh the cache immediately with revalidatePath or revalidateTag inside a Server Action or route handler:

jsx
import { revalidatePath } from 'next/cache'; revalidatePath('/blog');

Key Takeaways

  • Async Server Components fetch data with plain await - no client-side effects needed.
  • Choose per request (no-store), cached (force-cache), or timed (revalidate) explicitly.
  • Identical fetches are deduplicated; use React cache() for database queries.
  • loading.js and Suspense stream UI so slow data never blocks the page.

Next up: Lesson 8 applies these skills to dynamic routes - pages whose URLs contain parameters like /blog/my-post.

Data Fetching and Caching - Next.js | CodeYourCraft | CodeYourCraft