Loading...
Loading...
Provides Next.js App Router data fetching patterns including SWR and React Query integration, parallel data fetching, Incremental Static Regeneration (ISR), revalidation strategies, and error boundaries. Use when implementing data fetching in Next.js applications, choosing between server and client fetching, setting up caching strategies, or handling loading and error states.
npx skill4agent add giuseppe-trisciuoglio/developer-kit nextjs-data-fetchingasync function getPosts() {
const res = await fetch('https://api.example.com/posts');
if (!res.ok) throw new Error('Failed to fetch posts');
return res.json();
}
export default async function PostsPage() {
const posts = await getPosts();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}async function getDashboardData() {
const [user, posts, analytics] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/analytics').then(r => r.json()),
]);
return { user, posts, analytics };
}
export default async function DashboardPage() {
const { user, posts, analytics } = await getDashboardData();
// Render dashboard
}async function getUserPosts(userId: string) {
const user = await fetch(`/api/users/${userId}`).then(r => r.json());
const posts = await fetch(`/api/users/${userId}/posts`).then(r => r.json());
return { user, posts };
}async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: {
revalidate: 60 // Revalidate every 60 seconds
}
});
return res.json();
}revalidateTagrevalidatePath// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';
import { NextRequest } from 'next/server';
export async function POST(request: NextRequest) {
const tag = request.nextUrl.searchParams.get('tag');
if (tag) {
revalidateTag(tag);
return Response.json({ revalidated: true });
}
return Response.json({ revalidated: false }, { status: 400 });
}async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: {
tags: ['posts'],
revalidate: 3600
}
});
return res.json();
}// Dynamic rendering (no caching)
async function getRealTimeData() {
const res = await fetch('https://api.example.com/data', {
cache: 'no-store'
});
return res.json();
}
// Or use dynamic export
export const dynamic = 'force-dynamic';npm install swr'use client';
import useSWR from 'swr';
const fetcher = (url: string) => fetch(url).then(r => r.json());
export function Posts() {
const { data, error, isLoading } = useSWR('/api/posts', fetcher, {
refreshInterval: 5000,
revalidateOnFocus: true,
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Failed to load posts</div>;
return (
<ul>
{data.map((post: any) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}npm install @tanstack/react-query// app/providers.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useState } from 'react';
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
refetchOnWindowFocus: false,
},
},
}));
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
}'use client';
import { useQuery } from '@tanstack/react-query';
export function Posts() {
const { data, error, isLoading } = useQuery({
queryKey: ['posts'],
queryFn: async () => {
const res = await fetch('/api/posts');
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
},
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{data.map((post: any) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}// app/components/ErrorBoundary.tsx
'use client';
import { Component, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback: ReactNode;
}
interface State {
hasError: boolean;
}
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(): State {
return { hasError: true };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}// app/posts/page.tsx
import { ErrorBoundary } from '../components/ErrorBoundary';
import { Posts } from './Posts';
import { PostsError } from './PostsError';
export default function PostsPage() {
return (
<ErrorBoundary fallback={<PostsError />}>
<Posts />
</ErrorBoundary>
);
}'use client';
import { Component, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback: (props: { reset: () => void }) => ReactNode;
}
interface State {
hasError: boolean;
}
export class ErrorBoundary extends Component<Props, State> {
state = { hasError: false };
static getDerivedStateFromError(): State {
return { hasError: true };
}
reset = () => {
this.setState({ hasError: false });
};
render() {
if (this.state.hasError) {
return this.props.fallback({ reset: this.reset });
}
return this.props.children;
}
}// app/actions/posts.ts
'use server';
import { revalidateTag } from 'next/cache';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
const response = await fetch('https://api.example.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, content }),
});
if (!response.ok) {
throw new Error('Failed to create post');
}
revalidateTag('posts');
return response.json();
}// app/posts/CreatePostForm.tsx
'use client';
import { createPost } from '../actions/posts';
export function CreatePostForm() {
return (
<form action={createPost}>
<input name="title" placeholder="Title" required />
<textarea name="content" placeholder="Content" required />
<button type="submit">Create Post</button>
</form>
);
}// app/posts/loading.tsx
export default function PostsLoading() {
return (
<div className="space-y-4">
{[...Array(5)].map((_, i) => (
<div key={i} className="h-16 bg-gray-200 animate-pulse rounded" />
))}
</div>
);
}// app/posts/page.tsx
import { Suspense } from 'react';
import { PostsList } from './PostsList';
import { PostsSkeleton } from './PostsSkeleton';
import { PopularPosts } from './PopularPosts';
export default function PostsPage() {
return (
<div>
<h1>Posts</h1>
<Suspense fallback={<PostsSkeleton />}>
<PostsList />
</Suspense>
<Suspense fallback={<div>Loading popular...</div>}>
<PopularPosts />
</Suspense>
</div>
);
}Promise.all()cache: 'no-store'loading.tsxuseStateuseEffect'use client'fetch'use server'cache: 'force-cache'| Scenario | Solution |
|---|---|
| Static content, infrequent updates | Server Component + ISR |
| Dynamic content, user-specific | Server Component + |
| Real-time updates | Client Component + SWR/React Query |
| User interactions | Client Component + mutation library |
| Mixed requirements | Server for initial, Client for updates |
// app/blog/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 }
});
return res.json();
}
export default async function BlogPage() {
const posts = await getPosts();
return (
<main>
<h1>Blog Posts</h1>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</main>
);
}// app/dashboard/page.tsx
async function getDashboardData() {
const [user, stats, activity] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/stats').then(r => r.json()),
fetch('/api/activity').then(r => r.json()),
]);
return { user, stats, activity };
}
export default async function DashboardPage() {
const { user, stats, activity } = await getDashboardData();
return (
<div className="dashboard">
<UserProfile user={user} />
<StatsCards stats={stats} />
<ActivityFeed activity={activity} />
</div>
);
}// app/crypto/PriceTicker.tsx
'use client';
import useSWR from 'swr';
const fetcher = (url: string) => fetch(url).then(r => r.json());
export function PriceTicker() {
const { data, error } = useSWR('/api/crypto/prices', fetcher, {
refreshInterval: 5000,
revalidateOnFocus: true,
});
if (error) return <div>Failed to load prices</div>;
if (!data) return <div>Loading...</div>;
return (
<div className="ticker">
<span>BTC: ${data.bitcoin}</span>
<span>ETH: ${data.ethereum}</span>
</div>
);
}// app/actions/contact.ts
'use server';
import { revalidateTag } from 'next/cache';
export async function submitContact(formData: FormData) {
const data = {
name: formData.get('name'),
email: formData.get('email'),
message: formData.get('message'),
};
await fetch('https://api.example.com/contact', {
method: 'POST',
body: JSON.stringify(data),
});
revalidateTag('messages');
}// app/contact/page.tsx
import { submitContact } from '../actions/contact';
export default function ContactPage() {
return (
<form action={submitContact}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<textarea name="message" placeholder="Message" required />
<button type="submit">Send</button>
</form>
);
}