# Data Fetching & Caching ## Extended fetch API Next.js extends the native fetch with caching and revalidation options: ```tsx // app/page.tsx async function getData() { const res = await fetch("https://api.example.com/posts", { cache: "force-cache", // Default: cache forever (SSG) }) if (!res.ok) { throw new Error("Failed to fetch data") } return res.json() } export default async function Page() { const data = await getData() return
{/* render data */}
} ``` ## Cache Options ```tsx // 1. Force cache (Static Site Generation) fetch("https://api.example.com/data", { cache: "force-cache", // Default behavior }) // 2. No cache (Server-Side Rendering) fetch("https://api.example.com/data", { cache: "no-store", // Always fetch fresh data }) // 3. Revalidate (Incremental Static Regeneration) fetch("https://api.example.com/data", { next: { revalidate: 3600 }, // Revalidate every hour }) // 4. Revalidate with tags fetch("https://api.example.com/data", { next: { tags: ["posts"] }, }) ``` ## Revalidation Methods ### Time-based Revalidation (ISR) ```tsx // Revalidate every 60 seconds async function getPosts() { const res = await fetch("https://api.example.com/posts", { next: { revalidate: 60 }, }) return res.json() } // Route segment config export const revalidate = 60 // seconds export default async function Page() { const posts = await getPosts() return
{/* render */}
} ``` ### On-Demand Revalidation ```tsx // app/api/revalidate/route.ts import { revalidatePath, revalidateTag } from "next/cache" import { NextRequest } from "next/server" export async function POST(request: NextRequest) { const path = request.nextUrl.searchParams.get("path") if (path) { revalidatePath(path) return Response.json({ revalidated: true, now: Date.now() }) } return Response.json({ revalidated: false }) } // Usage in Server Action ;("use server") import { revalidatePath } from "next/cache" export async function createPost(data: FormData) { await db.post.create({ data }) // Revalidate specific path revalidatePath("/posts") // Revalidate entire layout revalidatePath("/posts", "layout") } ``` ### Tag-based Revalidation ```tsx // Fetch with tags async function getPosts() { const res = await fetch("https://api.example.com/posts", { next: { tags: ["posts"] }, }) return res.json() } async function getAuthors() { const res = await fetch("https://api.example.com/authors", { next: { tags: ["authors"] }, }) return res.json() } // Revalidate by tag import { revalidateTag } from "next/cache" export async function createPost() { // Revalidate all fetches tagged with 'posts' revalidateTag("posts") } ``` ## Route Segment Config ```tsx // app/posts/page.tsx // Force dynamic rendering export const dynamic = "force-dynamic" // 'auto' | 'force-dynamic' | 'error' | 'force-static' // Revalidation interval export const revalidate = 3600 // false | 0 | number (seconds) // Fetch cache export const fetchCache = "auto" // 'auto' | 'default-cache' | 'only-cache' | 'force-cache' | 'force-no-store' | 'default-no-store' | 'only-no-store' // Runtime export const runtime = "nodejs" // 'nodejs' | 'edge' // Preferred region export const preferredRegion = "auto" // 'auto' | 'home' | 'edge' | string | string[] export default async function Page() { return
Posts
} ``` ## Parallel Data Fetching ```tsx async function getUser() { return fetch("https://api.example.com/user") } async function getPosts() { return fetch("https://api.example.com/posts") } async function getComments() { return fetch("https://api.example.com/comments") } export default async function Page() { // Fetch in parallel with Promise.all const [user, posts, comments] = await Promise.all([getUser(), getPosts(), getComments()]) return (
) } ``` ## Sequential Data Fetching ```tsx // When one fetch depends on another export default async function Page({ params }: { params: { id: string } }) { // First fetch const user = await fetch(`https://api.example.com/users/${params.id}`).then((res) => res.json()) // Second fetch depends on first const posts = await fetch(`https://api.example.com/users/${user.id}/posts`).then((res) => res.json() ) return (

{user.name}

) } ``` ## Streaming with Suspense ```tsx // app/page.tsx import { Suspense } from "react" async function Posts() { const posts = await fetch("https://api.example.com/posts", { cache: "no-store", }).then((res) => res.json()) return ( ) } export default function Page() { return (

Posts

Loading posts...
}> ) } ``` ## React cache for Deduplication ```tsx // lib/data.ts import { cache } from "react" export const getUser = cache(async (id: string) => { const res = await fetch(`https://api.example.com/users/${id}`) return res.json() }) // components/user-profile.tsx export async function UserProfile({ userId }: { userId: string }) { const user = await getUser(userId) // Cached return
{user.name}
} // components/user-posts.tsx export async function UserPosts({ userId }: { userId: string }) { const user = await getUser(userId) // Uses cached result return
{user.posts.length} posts
} // app/page.tsx export default function Page() { return ( <> {/* Same fetch, deduplicated */} ) } ``` ## Database Queries ```tsx // lib/db.ts import { PrismaClient } from "@prisma/client" const globalForPrisma = global as unknown as { prisma: PrismaClient } export const db = globalForPrisma.prisma || new PrismaClient() if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db // app/posts/page.tsx import { db } from "@/lib/db" export const revalidate = 60 // Revalidate every 60 seconds export default async function PostsPage() { const posts = await db.post.findMany({ include: { author: true }, orderBy: { createdAt: "desc" }, }) return (
{posts.map((post) => (

{post.title}

By {post.author.name}

))}
) } ``` ## Error Handling ```tsx async function getData() { const res = await fetch("https://api.example.com/data") if (!res.ok) { // This will activate the closest error.tsx throw new Error("Failed to fetch data") } return res.json() } export default async function Page() { const data = await getData() return
{data.title}
} // app/error.tsx ;("use client") export default function Error({ error, reset, }: { error: Error & { digest?: string } reset: () => void }) { return (

Something went wrong!

) } ``` ## Loading States ```tsx // app/posts/loading.tsx export default function Loading() { return
Loading posts...
} // app/posts/page.tsx export default async function PostsPage() { const posts = await fetch("https://api.example.com/posts").then((res) => res.json()) return
{/* render posts */}
} ``` ## Client-Side Data Fetching ```tsx // When you need client-side fetching "use client" import useSWR from "swr" const fetcher = (url: string) => fetch(url).then((res) => res.json()) export function Posts() { const { data, error, isLoading } = useSWR("/api/posts", fetcher, { refreshInterval: 3000, // Refresh every 3 seconds }) if (error) return
Failed to load
if (isLoading) return
Loading...
return (
    {data.map((post: Post) => (
  • {post.title}
  • ))}
) } ``` ## Preloading Data ```tsx // lib/data.ts import { cache } from "react" export const preload = (id: string) => { void getUser(id) // Trigger fetch without awaiting } export const getUser = cache(async (id: string) => { return fetch(`https://api.example.com/users/${id}`).then((res) => res.json()) }) // components/user.tsx import { getUser, preload } from "@/lib/data" export async function User({ id }: { id: string }) { const user = await getUser(id) return
{user.name}
} // app/page.tsx import { User } from "@/components/user" import { preload } from "@/lib/data" export default async function Page() { preload("123") // Start loading immediately return } ``` ## Static Generation with Dynamic Routes ```tsx // app/posts/[slug]/page.tsx type Post = { slug: string title: string content: string } export async function generateStaticParams() { const posts = await fetch("https://api.example.com/posts").then((res) => res.json()) return posts.map((post: Post) => ({ slug: post.slug, })) } export default async function Post({ params }: { params: { slug: string } }) { const post = await fetch(`https://api.example.com/posts/${params.slug}`).then((res) => res.json()) return (

{post.title}

{post.content}
) } ``` ## Quick Reference | Strategy | Config | Use Case | | ------------- | ---------------------------------------- | ---------------------- | | **SSG** | `cache: 'force-cache'` | Static content | | **SSR** | `cache: 'no-store'` | Always fresh data | | **ISR** | `next: { revalidate: 60 }` | Periodic updates | | **Tag-based** | `next: { tags: ['posts'] }` | On-demand revalidation | | **Dynamic** | `export const dynamic = 'force-dynamic'` | Per-request data | ## Best Practices 1. **Default to caching** - Use force-cache for static content 2. **Use ISR** - Revalidate periodically for semi-dynamic content 3. **Parallel fetching** - Use Promise.all for independent requests 4. **Deduplicate** - Use React cache() for repeated calls 5. **Stream with Suspense** - Show content progressively 6. **Tag your fetches** - Enable granular revalidation 7. **Handle errors** - Use error.tsx for graceful degradation