--- name: nextjs-data-fetching user-invocable: false description: Use when next.js data fetching patterns including SSG, SSR, and ISR. Use when building data-driven Next.js applications. allowed-tools: - Bash - Read --- # Next.js Data Fetching Master data fetching in Next.js with static generation, server-side rendering, and incremental static regeneration. ## Fetch with Caching Strategies ```typescript // Default: 'force-cache' (similar to SSG) export default async function Page() { const data = await fetch('https://api.example.com/data'); const json = await data.json(); return
{json.title}
; } // No caching: 'no-store' (similar to SSR) export default async function DynamicPage() { const data = await fetch('https://api.example.com/data', { cache: 'no-store' }); const json = await data.json(); return
{json.title}
; } // Revalidate every 60 seconds (ISR) export default async function RevalidatedPage() { const data = await fetch('https://api.example.com/data', { next: { revalidate: 60 } }); const json = await data.json(); return
{json.title}
; } // Per-route caching config export const revalidate = 3600; // Revalidate every hour export default async function Page() { const data = await fetch('https://api.example.com/data'); return
{data.title}
; } // Dynamic rendering export const dynamic = 'force-dynamic'; // Equivalent to cache: 'no-store' export const dynamic = 'force-static'; // Equivalent to cache: 'force-cache' export const dynamic = 'error'; // Error if dynamic functions used export const dynamic = 'auto'; // Default behavior ``` ## Static Generation with generateStaticParams ```typescript // app/posts/[id]/page.tsx interface Post { id: string; title: string; content: string; } // Generate static paths at build time export async function generateStaticParams() { const posts = await fetch('https://api.example.com/posts').then(r => r.json()); return posts.map((post: Post) => ({ id: post.id.toString() })); } export default async function Post({ params }: { params: { id: string } }) { const post = await fetch(`https://api.example.com/posts/${params.id}`, { next: { revalidate: 3600 } // Revalidate hourly }).then(r => r.json()); return (

{post.title}

{post.content}

); } // Multiple dynamic segments // app/shop/[category]/[product]/page.tsx export async function generateStaticParams() { const categories = await getCategories(); const paths = await Promise.all( categories.map(async (category) => { const products = await getProducts(category.slug); return products.map((product) => ({ category: category.slug, product: product.slug })); }) ); return paths.flat(); } export default async function Product({ params }: { params: { category: string; product: string } }) { const product = await getProduct(params.category, params.product); return (

{product.name}

{product.description}

); } // Limit static generation for large datasets export const dynamicParams = true; // Default: generate on-demand export const dynamicParams = false; // Return 404 for ungenerated paths export async function generateStaticParams() { // Only generate top 100 posts at build time const posts = await getPosts({ limit: 100 }); return posts.map((post) => ({ id: post.id })); } ``` ## Time-Based Revalidation (ISR) ```typescript // Page-level revalidation export const revalidate = 60; // Revalidate every 60 seconds export default async function Page() { const posts = await fetch('https://api.example.com/posts').then(r => r.json()); return (
{posts.map(post => (

{post.title}

))}
); } // Per-request revalidation export default async function Page() { const staticData = await fetch('https://api.example.com/static', { next: { revalidate: 3600 } // Cache for 1 hour }).then(r => r.json()); const dynamicData = await fetch('https://api.example.com/dynamic', { next: { revalidate: 10 } // Cache for 10 seconds }).then(r => r.json()); return (
Static: {staticData.value}
Dynamic: {dynamicData.value}
); } // Mixed caching strategies export default async function Dashboard() { // Never cache (always fresh) const liveData = await fetch('https://api.example.com/live', { cache: 'no-store' }).then(r => r.json()); // Cache indefinitely (build-time only) const staticData = await fetch('https://api.example.com/static', { cache: 'force-cache' }).then(r => r.json()); // Revalidate periodically const periodicData = await fetch('https://api.example.com/periodic', { next: { revalidate: 300 } }).then(r => r.json()); return (
); } ``` ## On-Demand Revalidation ```typescript // app/api/revalidate/route.ts import { revalidatePath, revalidateTag } from 'next/cache'; import { NextRequest, NextResponse } from 'next/server'; export async function POST(request: NextRequest) { const secret = request.nextUrl.searchParams.get('secret'); // Verify secret token if (secret !== process.env.REVALIDATE_SECRET) { return NextResponse.json({ error: 'Invalid secret' }, { status: 401 }); } const path = request.nextUrl.searchParams.get('path'); if (path) { // Revalidate specific path revalidatePath(path); return NextResponse.json({ revalidated: true, path }); } return NextResponse.json({ error: 'Missing path' }, { status: 400 }); } // app/posts/[slug]/page.tsx - Using cache tags export default async function Post({ params }: { params: { slug: string } }) { const post = await fetch(`https://api.example.com/posts/${params.slug}`, { next: { tags: ['posts', `post-${params.slug}`] } }).then(r => r.json()); return
{post.content}
; } // app/api/revalidate-tag/route.ts export async function POST(request: NextRequest) { const tag = request.nextUrl.searchParams.get('tag'); if (tag) { revalidateTag(tag); return NextResponse.json({ revalidated: true, tag }); } return NextResponse.json({ error: 'Missing tag' }, { status: 400 }); } // Server Action for revalidation 'use server'; import { revalidatePath } from 'next/cache'; export async function updatePost(id: string, data: FormData) { await db.post.update({ where: { id }, data }); // Revalidate the post page revalidatePath(`/posts/${id}`); // Revalidate the posts list revalidatePath('/posts'); } ``` ## Parallel Data Fetching ```typescript // Fetch multiple resources in parallel export default async function Page() { const [posts, categories, tags] = await Promise.all([ fetch('https://api.example.com/posts').then(r => r.json()), fetch('https://api.example.com/categories').then(r => r.json()), fetch('https://api.example.com/tags').then(r => r.json()) ]); return (
); } // Parallel fetching with different cache strategies export default async function Dashboard() { const [stats, recentActivity, settings] = await Promise.all([ fetch('https://api.example.com/stats', { next: { revalidate: 60 } }).then(r => r.json()), fetch('https://api.example.com/activity', { cache: 'no-store' }).then(r => r.json()), fetch('https://api.example.com/settings', { cache: 'force-cache' }).then(r => r.json()) ]); return (
); } // Fetching with fallbacks export default async function Page() { const results = await Promise.allSettled([ fetch('https://api.example.com/required').then(r => r.json()), fetch('https://api.example.com/optional1').then(r => r.json()), fetch('https://api.example.com/optional2').then(r => r.json()) ]); const [required, optional1, optional2] = results; return (
{required.status === 'fulfilled' && } {optional1.status === 'fulfilled' && } {optional2.status === 'fulfilled' && }
); } ``` ## Streaming and Suspense ```typescript // app/posts/page.tsx import { Suspense } from 'react'; export default function PostsPage() { return (

Blog Posts

{/* Stream featured posts */} }> {/* Stream all posts */} }> {/* Stream comments */} }>
); } async function FeaturedPosts() { const posts = await fetch('https://api.example.com/posts/featured', { next: { revalidate: 300 } }).then(r => r.json()); return (
{posts.map(post => ( ))}
); } async function AllPosts() { const posts = await fetch('https://api.example.com/posts', { next: { revalidate: 60 } }).then(r => r.json()); return (
{posts.map(post => ( ))}
); } async function RecentComments() { const comments = await fetch('https://api.example.com/comments/recent', { cache: 'no-store' }).then(r => r.json()); return (
{comments.map(comment => ( ))}
); } ``` ## Loading States ```typescript // app/posts/loading.tsx export default function Loading() { return (
); } // Custom loading component with Suspense export default function Page() { return (
}> }>
); } function CustomLoading({ message }: { message: string }) { return (

{message}

); } // Progressive enhancement with instant loading UI export default function Page() { return (
{/* Shows immediately */} {/* Streams in as ready */} }> }>
); } ``` ## Error Handling ```typescript // app/posts/error.tsx 'use client'; export default function Error({ error, reset }: { error: Error & { digest?: string }; reset: () => void; }) { return (

Failed to load posts

{error.message}

); } // Handling fetch errors export default async function Page() { try { const data = await fetch('https://api.example.com/data', { next: { revalidate: 60 } }); if (!data.ok) { throw new Error(`Failed to fetch: ${data.status}`); } const json = await data.json(); return ; } catch (error) { console.error('Fetch error:', error); return ; } } // Graceful degradation export default async function Page() { let data; try { const res = await fetch('https://api.example.com/data'); data = await res.json(); } catch (error) { console.error('Failed to fetch data:', error); data = null; } return (
{data ? ( ) : (

Unable to load content

)}
); } // Error boundaries with retry logic 'use client'; import { useState } from 'react'; export default function ErrorWithRetry({ error, reset }: { error: Error; reset: () => void; }) { const [retrying, setRetrying] = useState(false); const handleRetry = async () => { setRetrying(true); await new Promise(resolve => setTimeout(resolve, 1000)); reset(); setRetrying(false); }; return (

Something went wrong

{error.message}

); } ``` ## Server Actions for Mutations ```typescript // app/actions.ts 'use server'; import { revalidatePath } from 'next/cache'; export async function createPost(formData: FormData) { const title = formData.get('title') as string; const content = formData.get('content') as string; try { const res = await fetch('https://api.example.com/posts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title, content }) }); if (!res.ok) { throw new Error('Failed to create post'); } const post = await res.json(); // Revalidate the posts page revalidatePath('/posts'); return { success: true, post }; } catch (error) { return { success: false, error: error.message }; } } export async function updatePost(id: string, formData: FormData) { const title = formData.get('title') as string; const content = formData.get('content') as string; await fetch(`https://api.example.com/posts/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title, content }) }); revalidatePath(`/posts/${id}`); revalidatePath('/posts'); } export async function deletePost(id: string) { await fetch(`https://api.example.com/posts/${id}`, { method: 'DELETE' }); revalidatePath('/posts'); } // app/posts/new/page.tsx import { createPost } from '../actions'; export default function NewPost() { return (