# 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