Developer-kit nextjs-data-fetching
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.
git clone https://github.com/giuseppe-trisciuoglio/developer-kit
T=$(mktemp -d) && git clone --depth=1 https://github.com/giuseppe-trisciuoglio/developer-kit "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/developer-kit-typescript/skills/nextjs-data-fetching" ~/.claude/skills/giuseppe-trisciuoglio-developer-kit-nextjs-data-fetching && rm -rf "$T"
plugins/developer-kit-typescript/skills/nextjs-data-fetching/SKILL.mdNext.js Data Fetching
Overview
Provides patterns for data fetching in Next.js App Router: server-side fetching, SWR/React Query integration, ISR, revalidation, error boundaries, and loading states.
When to Use
- Implementing data fetching in Next.js App Router
- Choosing between Server Components and Client Components
- Setting up SWR or React Query for client-side caching
- Configuring ISR, time-based, or on-demand revalidation
- Handling loading and error states
- Building forms with Server Actions
Instructions
Server Component Fetching
Fetch directly in async Server Components:
async 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> ); }
Parallel Data Fetching
Use
Promise.all() for independent requests:
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 }
Sequential Data Fetching (When Dependencies Exist)
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 }; }
Time-based Revalidation (ISR)
async function getPosts() { const res = await fetch('https://api.example.com/posts', { next: { revalidate: 60 } // Revalidate every 60 seconds }); return res.json(); }
On-Demand Revalidation
// 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 }); }
Tag data for selective revalidation:
async function getPosts() { const res = await fetch('https://api.example.com/posts', { next: { tags: ['posts'], revalidate: 3600 } }); return res.json(); }
Opt-out of Caching
async function getRealTimeData() { const res = await fetch('https://api.example.com/data', { cache: 'no-store' }); return res.json(); } // Or: export const dynamic = 'force-dynamic';
Client-Side Data Fetching
SWR Integration
Install:
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> ); }
React Query Integration
Install:
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> ); }
See react-query.md for mutations, optimistic updates, infinite queries, and advanced patterns.
Error Boundaries
Wrap client-side data fetching in Error Boundaries to handle failures gracefully:
See error-boundaries.md for full
ErrorBoundary implementations (basic, with reset callback) and usage examples with data fetching.
Server Actions
Use Server Actions for mutations with cache revalidation:
See server-actions.md for complete examples including form validation with
useActionState, error handling, and cache invalidation.
Loading States
loading.tsx Pattern
// 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> ); }
Suspense Boundaries
// app/posts/page.tsx import { Suspense } from 'react'; import { PostsList } from './PostsList'; import { PostsSkeleton } from './PostsSkeleton'; export default function PostsPage() { return ( <div> <h1>Posts</h1> <Suspense fallback={<PostsSkeleton />}> <PostsList /> </Suspense> </div> ); }
Best Practices
- Default to Server Components — Fetch in Server Components for better performance
- Use parallel fetching —
for independent requests to reduce latencyPromise.all() - Choose appropriate caching:
- Static data: long revalidation intervals
- Dynamic data: short revalidation or
cache: 'no-store' - User-specific data: use dynamic rendering
- Handle errors gracefully — Wrap client data fetching in error boundaries
- Implement loading states — Use
or Suspense boundariesloading.tsx - Prefer SWR/React Query for: real-time data, user interactions, background updates
- Use Server Actions for: form submissions, mutations requiring cache revalidation
Constraints and Warnings
Critical Constraints
- Server Components cannot use hooks (
,useState
) or client data fetching librariesuseEffect - Client Components must include the
directive'use client' - The
API in Next.js extends standard Web fetch with Next.js-specific caching optionsfetch - Server Actions require
and can only be called from Client Components or form actions'use server'
Common Pitfalls
- Fetching in loops — Avoid sequential fetches in Server Components; use parallel fetching
- Cache poisoning — Do not use
for user-specific or personalized dataforce-cache - Memory leaks — Clean up subscriptions in Client Components when using real-time data
- Hydration mismatches — Ensure server and client render the same initial state with React Query hydration
Examples
Example 1: Blog with ISR
Input: Create a blog page that fetches posts and updates every hour.
// 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> ); }
Output: Page statically generated at build time, revalidated every hour.
Example 2: Dashboard with Parallel Fetching
Input: Build a dashboard showing user profile, stats, and recent activity in parallel.
// 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> ); }
Output: All three requests execute concurrently, minimizing total load time.