← Back to chapters
08intermediate

Data Fetching & Caching

Fetching data in Server Components, caching strategies, revalidation, and streaming.

Data Fetching in the App Router

Forget getServerSideProps and getStaticProps. Those are Pages Router (old). In the App Router, you just... fetch data. In your component. Because Server Components can be async.

Basic Fetching

src/app/posts/page.tsx
tsx
1// src/app/posts/page.tsx
2// This component runs on the server, fetch happens server-side
3
4interface Post {
5id: number;
6title: string;
7body: string;
8}
9
10export default async function PostsPage() {
11const res = await fetch("https://jsonplaceholder.typicode.com/posts");
12const posts: Post[] = await res.json();
13
14return (
15 <ul>
16 {posts.map((post) => (
17 <li key={post.id}>
18 <h2>{post.title}</h2>
19 <p>{post.body}</p>
20 </li>
21 ))}
22 </ul>
23);
24}
šŸ’”

No useEffect needed

In Server Components, you just await your data directly. No loading states to manage, no useEffect + useState dance. The component waits for data, renders HTML, and sends it to the client. Clean.

Caching Behavior

Next.js 15 changed the caching defaults. Fetch requests are NOT cached by default anymore (they were in v14). You now opt-in to caching:

Caching strategies
tsx
1// No caching (default in Next.js 15)
2const data = await fetch("https://api.example.com/data");
3
4// Cache indefinitely (like static generation)
5const data = await fetch("https://api.example.com/data", {
6cache: "force-cache",
7});
8
9// Revalidate every 60 seconds
10const data = await fetch("https://api.example.com/data", {
11next: { revalidate: 60 },
12});
13
14// Revalidate based on tags (for on-demand revalidation)
15const data = await fetch("https://api.example.com/posts", {
16next: { tags: ["posts"] },
17});

Streaming with Suspense

Don't want the entire page to wait for slow data? Use Suspense to stream parts independently:

src/app/dashboard/page.tsx
tsx
1import { Suspense } from "react";
2
3export default function DashboardPage() {
4return (
5 <div>
6 <h1>Dashboard</h1>
7
8 {/* This loads instantly */}
9 <WelcomeMessage />
10
11 {/* This streams in when ready */}
12 <Suspense fallback={<p>Loading stats...</p>}>
13 <SlowStats />
14 </Suspense>
15
16 {/* This streams independently */}
17 <Suspense fallback={<p>Loading feed...</p>}>
18 <ActivityFeed />
19 </Suspense>
20 </div>
21);
22}
23
24async function SlowStats() {
25const stats = await fetch("https://api.example.com/stats");
26// ... render stats
27}
šŸš€

Streaming is a superpower

With Suspense, the page shell renders immediately, then each section pops in as its data arrives.
• Users see content faster
• Slow APIs don't block the entire page
• Each section loads independently
Use this everywhere.

The 'use cache' Directive (Next.js 16)

Next.js 16 introduces the 'use cache' directive. Instead of configuring caching per-fetch, you can mark entire functions or components as cacheable:

use cache example
tsx
1// Cache an entire async function
2async function getProducts() {
3"use cache";
4const res = await fetch("https://api.example.com/products");
5return res.json();
6}
7
8// Cache a component
9async function ProductList() {
10"use cache";
11const products = await getProducts();
12return (
13 <ul>
14 {products.map((p) => <li key={p.id}>{p.name}</li>)}
15 </ul>
16);
17}
šŸš€

use cache vs fetch options

The 'use cache' directive caches the entire function result, not just individual fetch calls.
• Simpler than per-fetch config
• Pair with cacheLife() for expiration control
• Pair with cacheTag() for on-demand revalidation

ā–¶Watch and Learn