nextjs-data-fetching

Next.js Data Fetching

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "nextjs-data-fetching" with this command: npx skills add giuseppe-trisciuoglio/developer-kit/giuseppe-trisciuoglio-developer-kit-nextjs-data-fetching

Next.js Data Fetching

Overview

This skill provides comprehensive patterns for data fetching in Next.js App Router applications. It covers server-side fetching, client-side libraries integration, caching strategies, error handling, and loading states.

When to Use

Use this skill for:

  • Implementing data fetching in Next.js App Router

  • Choosing between Server Components and Client Components for data fetching

  • Setting up SWR or React Query integration

  • Implementing parallel data fetching patterns

  • Configuring ISR and revalidation strategies

  • Creating error boundaries for data fetching

Instructions

Server Component Fetching (Default)

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

Fetch multiple resources in parallel:

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 }; }

Caching and Revalidation

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

Use route handlers with revalidateTag or revalidatePath :

// 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 cached 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

// 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';

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

Setup provider:

// 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 in components:

'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> ); }

See REACT-QUERY.md for advanced patterns.

Error Boundaries

Creating Error Boundaries

// 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;

} }

Using Error Boundaries with Data Fetching

// 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> ); }

Error Boundary with Reset

'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;

} }

Server Actions for Mutations

// 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> ); }

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'; import { PopularPosts } from './PopularPosts';

export default function PostsPage() { return ( <div> <h1>Posts</h1>

  &#x3C;Suspense fallback={&#x3C;PostsSkeleton />}>
    &#x3C;PostsList />
  &#x3C;/Suspense>

  &#x3C;Suspense fallback={&#x3C;div>Loading popular...&#x3C;/div>}>
    &#x3C;PopularPosts />
  &#x3C;/Suspense>
&#x3C;/div>

); }

Best Practices

Default to Server Components - Fetch data in Server Components when possible for better performance

Use parallel fetching - Use Promise.all() for independent data requests

Choose appropriate caching:

  • Static data: Long revalidation intervals or no revalidation

  • Dynamic data: Short revalidation or cache: 'no-store'

  • User-specific: Use dynamic rendering

Handle errors gracefully - Wrap client data fetching in error boundaries

Use loading states - Implement loading.tsx or Suspense boundaries

Prefer SWR/React Query for:

  • Real-time data

  • User interactions requiring immediate feedback

  • Data that needs background updates

Use Server Actions for:

  • Form submissions

  • Mutations that need to revalidate cache

  • Operations requiring server-side logic

Constraints and Warnings

Critical Constraints

  • Server Components cannot use hooks like useState , useEffect , or data fetching libraries (SWR, React Query)

  • Client Components must include the 'use client' directive

  • The fetch API in Next.js extends the standard Web API with Next.js-specific caching options

  • Server Actions require the 'use server' directive and can only be called from Client Components or form actions

Common Pitfalls

  • Fetching in loops: Avoid fetching data inside loops in Server Components; use parallel fetching instead

  • Cache poisoning: Be careful with cache: 'force-cache' for user-specific data

  • Memory leaks: Always clean up subscriptions in Client Components when using real-time data

  • Hydration mismatches: Ensure server and client render the same initial state when using React Query hydration

Decision Matrix

Scenario Solution

Static content, infrequent updates Server Component + ISR

Dynamic content, user-specific Server Component + cache: 'no-store'

Real-time updates Client Component + SWR/React Query

User interactions Client Component + mutation library

Mixed requirements Server for initial, Client for updates

Examples

Example 1: Basic Server Component 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: Parallel Data Fetching for Dashboard

Input: Build a dashboard showing user profile, stats, and recent activity.

// 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, reducing total load time.

Example 3: Real-time Data with SWR

Input: Display live cryptocurrency prices that update every 5 seconds.

// 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> ); }

Output: Component displays live-updating prices with automatic refresh.

Example 4: Form Submission with Server Action

Input: Create a contact form that submits data and refreshes the cache.

// 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> ); }

Output: Form submits via Server Action, cache is invalidated on success.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

shadcn-ui

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

tailwind-css-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

unit-test-bean-validation

No summary provided by upstream source.

Repository SourceNeeds Review