| 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
export default async function Page() {
const data = await fetch('https://api.example.com/data');
const json = await data.json();
return <div>{json.title}</div>;
}
export default async function DynamicPage() {
const data = await fetch('https://api.example.com/data', {
cache: 'no-store'
});
const json = await data.json();
return <div>{json.title}</div>;
}
export default async function RevalidatedPage() {
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 60 }
});
const json = await data.json();
return <div>{json.title}</div>;
}
export const revalidate = 3600;
export default async function Page() {
const data = await fetch('https://api.example.com/data');
return <div>{data.title}</div>;
}
export const dynamic = 'force-dynamic';
export const dynamic = 'force-static';
export const dynamic = 'error';
export const dynamic = 'auto';
Static Generation with generateStaticParams
interface Post {
id: string;
title: string;
content: string;
}
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 }
}).then(r => r.json());
return (
);
}
() {
categories = ();
paths = .(
categories.( (category) => {
products = (category.);
products.( ({
: category.,
: product.
}));
})
);
paths.();
}
() {
product = (params., params.);
(
);
}
dynamicParams = ;
dynamicParams = ;
() {
posts = ({ : });
posts.( ({
: post.
}));
}
Time-Based Revalidation (ISR)
export const revalidate = 60;
export default async function Page() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return (
<div>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
</article>
))}
</div>
);
}
export default async function Page() {
const staticData = await fetch('https://api.example.com/static', {
next: { revalidate: 3600 }
}).then(r => r.json());
const dynamicData = (, {
: { : }
}).( r.());
(
);
}
() {
liveData = (, {
:
}).( r.());
staticData = (, {
:
}).( r.());
periodicData = (, {
: { : }
}).( r.());
(
);
}
On-Demand Revalidation
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');
if (secret !== process.env.REVALIDATE_SECRET) {
return NextResponse.json({ error: 'Invalid secret' }, { status: 401 });
}
const path = request.nextUrl.searchParams.get('path');
if (path) {
revalidatePath(path);
return NextResponse.json({ revalidated: true, path });
}
return NextResponse.json({ error: 'Missing path' }, { status: });
}
() {
post = (, {
: { : [, ] }
}).( r.());
;
}
() {
tag = request...();
(tag) {
(tag);
.({ : , tag });
}
.({ : }, { : });
}
;
{ revalidatePath } ;
() {
db..({ : { id }, data });
();
();
}
Parallel Data Fetching
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 (
<div>
<PostList posts={posts} />
<CategoryList categories={categories} />
<TagCloud tags={tags} />
</div>
);
}
export default async function Dashboard() {
[stats, recentActivity, settings] = .([
(, {
: { : }
}).( r.()),
(, {
:
}).( r.()),
(, {
:
}).( r.())
]);
(
);
}
() {
results = .([
().( r.()),
().( r.()),
().( r.())
]);
[required, optional1, optional2] = results;
(
);
}
Streaming and Suspense
import { Suspense } from 'react';
export default function PostsPage() {
return (
<div>
<h1>Blog Posts</h1>
{/* Stream featured posts */}
<Suspense fallback={<FeaturedSkeleton />}>
<FeaturedPosts />
</Suspense>
{/* Stream all posts */}
<Suspense fallback={<PostsSkeleton />}>
<AllPosts />
</Suspense>
{/* Stream comments */}
<Suspense fallback={<CommentsSkeleton />}>
<RecentComments />
</Suspense>
</div>
);
}
async function FeaturedPosts() {
const posts = await fetch('https://api.example.com/posts/featured', {
next: { : }
}).( r.());
(
);
}
() {
posts = (, {
: { : }
}).( r.());
(
);
}
() {
comments = (, {
:
}).( r.());
(
);
}
Loading States
export default function Loading() {
return (
<div className="loading">
<div className="skeleton skeleton-title" />
<div className="skeleton skeleton-text" />
<div className="skeleton skeleton-text" />
<div className="skeleton skeleton-text" />
</div>
);
}
export default function Page() {
return (
<div>
<Suspense fallback={<CustomLoading message="Loading posts..." />}>
<Posts />
</Suspense>
<Suspense fallback={<CustomLoading message="Loading comments..." />}>
< />
);
}
() {
(
);
}
() {
(
);
}
Error Handling
'use client';
export default function Error({
error,
reset
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div className="error">
<h2>Failed to load posts</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
);
}
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.();
;
} (error) {
.(, error);
;
}
}
() {
data;
{
res = ();
data = res.();
} (error) {
.(, error);
data = ;
}
(
);
}
;
{ useState } ;
() {
[retrying, setRetrying] = ();
= () => {
();
( (resolve, ));
();
();
};
(
);
}
Server Actions for Mutations
'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();
revalidatePath('/posts');
return { success: true, post };
} catch (error) {
{ : , : error. };
}
}
() {
title = formData.() ;
content = formData.() ;
(, {
: ,
: { : },
: .({ title, content })
});
();
();
}
() {
(, {
:
});
();
}
{ createPost } ;
() {
(
);
}
;
{ useFormState, useFormStatus } ;
{ createPost } ;
() {
{ pending } = ();
(
);
}
() {
[state, formAction] = (createPost, );
(
);
}
Request Memoization
async function getUser(id: string) {
const res = await fetch(`https://api.example.com/users/${id}`);
return res.json();
}
export default async function Page() {
const user1 = await getUser('1');
const user2 = await getUser('1');
const user3 = await getUser('1');
return <div>{user1.name}</div>;
}
async function UserHeader() {
const user = await getUser('1');
return <header>{user.name};
}
() {
user = ();
;
}
() {
(
);
}
{ cache } ;
getUser = ( (: ) => {
res = ();
res.();
});
Database Queries
import { db } from '@/lib/db';
export default async function Posts() {
const posts = await db.post.findMany({
where: { published: true },
orderBy: { createdAt: 'desc' },
take: 10
});
return (
<div>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
</article>
))}
</div>
);
}
export default async function Post({ params }: { params: { id: string } }) {
const post = await db.post.findUnique({
where: { id: params. },
: {
: ,
: {
: {
:
},
: { : }
}
}
});
(!post) {
();
}
(
);
}
() {
[totalPosts, totalUsers, recentActivity] = .([
db..(),
db..(),
db..({
: ,
: { : }
})
]);
(
);
}
Pagination Patterns
export default async function Posts({
searchParams
}: {
searchParams: { cursor?: string }
}) {
const cursor = searchParams.cursor;
const posts = await db.post.findMany({
take: 10,
skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined,
orderBy: { createdAt: 'desc' }
});
const lastPost = posts[posts.length - 1];
const nextCursor = lastPost?.id;
return (
<div>
<PostList posts={posts} />
{nextCursor && (
<Link href={`/posts?cursor=${nextCursor}`}>Load More</Link>
)}
</div>
);
}
export default async function Posts() {
page = (searchParams. || );
perPage = ;
[posts, total] = .([
db..({
: (page - ) * perPage,
: perPage,
: { : }
}),
db..()
]);
totalPages = .(total / perPage);
(
);
}
;
{ useState } ;
{ loadMorePosts } ;
() {
[posts, setPosts] = (initialPosts);
[loading, setLoading] = ();
= () => {
();
lastId = posts[posts. - ].;
newPosts = (lastId);
([...posts, ...newPosts]);
();
};
(
);
}
When to Use This Skill
Use nextjs-data-fetching when you need to:
- Build static sites with dynamic data
- Implement SSR for dynamic content
- Use ISR for best of both worlds
- Optimize for SEO and performance
- Cache and revalidate data
- Build e-commerce sites
- Create content-heavy applications
- Implement real-time updates
- Build scalable applications
- Handle large datasets efficiently
- Implement pagination and infinite scroll
- Optimize Core Web Vitals
Best Practices
-
Use static generation by default - Leverage SSG for pages that can be
pre-rendered at build time for optimal performance.
-
Implement ISR for frequently updated content - Use time-based or on-demand
revalidation for dynamic content that doesn't need real-time updates.
-
Cache API responses appropriately - Set proper revalidate times based on
how frequently data changes.
-
Use TypeScript for data types - Define proper types for API responses and
database queries to catch errors early.
-
Handle loading and error states - Implement loading.tsx and error.tsx files
for better user experience.
-
Implement proper revalidation strategies - Use on-demand revalidation with
webhooks for immediate updates when data changes.
-
Optimize for Core Web Vitals - Use streaming and Suspense to improve
perceived performance and loading times.
-
Use parallel data fetching - Fetch independent data sources simultaneously
to reduce waterfall effects.
-
Test data fetching patterns - Verify caching behavior, revalidation, and
error handling in tests.
-
Monitor performance metrics - Track cache hit rates, revalidation
frequency, and page load times.
Common Pitfalls
-
Not caching data appropriately - Using cache: 'no-store' for everything
defeats the performance benefits of SSG/ISR.
-
Overusing SSR for static content - Rendering static content on every
request wastes server resources.
-
Not implementing error boundaries - Missing error.tsx files cause poor
user experience when data fetching fails.
-
Ignoring revalidation strategies - Not setting revalidate times leads to
stale data or too many unnecessary requests.
-
Not handling race conditions - Parallel requests without proper ordering
can cause inconsistent UI state.
-
Missing loading states - Not implementing loading.tsx or Suspense boundaries
creates jarring loading experiences.
-
Not optimizing bundle size - Fetching too much data or including unnecessary
fields increases payload size.
-
Exposing sensitive API keys - Accidentally exposing secrets in client
components or client-side fetches.
-
Not testing edge cases - Skipping tests for error states, empty data, and
loading states leads to poor UX.
-
Misunderstanding caching behavior - Not knowing when Next.js caches requests
can lead to stale data or performance issues.
Resources