
Nextjs Data Fetching
- 1.6k installs
- 311 repo stars
- Updated June 22, 2026
- giuseppe-trisciuoglio/developer-kit
nextjs-data-fetching is an agent skill that provides next.js app router data fetching patterns including swr and react query integration, parallel data fetching, incremental static regeneration (isr), revalidation strate
About
nextjs-data-fetching is an agent skill from giuseppe-trisciuoglio/developer-kit that 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 boundari. # Next.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 o Developers invoke nextjs-data-fetching during build/backend work for backend & apis tasks. The skill documents triggers, prerequisites, and step-by-step workflows grounded in SKILL.md. Compatible with Claude Code, Cursor, and Codex agent runtimes that load marketplace skills.
- 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
Nextjs Data Fetching by the numbers
- 1,552 all-time installs (skills.sh)
- +61 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #312 of 4,386 Backend & APIs skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
nextjs-data-fetching capabilities & compatibility
- Capabilities
- implementing data fetching in next.js app router · choosing between server components and client co · setting up swr or react query for client side ca · configuring isr, time based, or on demand revali · handling loading and error states
- Use cases
- orchestration
What nextjs-data-fetching says it does
Provides patterns for data fetching in Next.js App Router: server-side fetching, SWR/React Query integration, ISR, revalidation, error boundaries, and loading states.
- Implementing data fetching in Next.js App Router
- Choosing between Server Components and Client Components
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill nextjs-data-fetchingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.6k |
|---|---|
| repo stars | ★ 311 |
| Security audit | 3 / 3 scanners passed |
| Last updated | June 22, 2026 |
| Repository | giuseppe-trisciuoglio/developer-kit ↗ |
What it does
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 boundari
Who is it for?
Developers working on backend & apis during build tasks.
Skip if: Tasks outside Backend & APIs scope described in SKILL.md.
When should I use this skill?
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 boundari
What you get
Completed backend & apis workflow aligned with SKILL.md steps.
- Cached fetch functions
- Revalidation Route Handlers
- Server Action invalidation patterns
Files
Next.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
1. Default to Server Components — Fetch in Server Components for better performance 2. Use parallel fetching — Promise.all() for independent requests to reduce latency 3. Choose appropriate caching:
- Static data: long revalidation intervals
- Dynamic data: short revalidation or
cache: 'no-store' - User-specific data: use dynamic rendering
4. Handle errors gracefully — Wrap client data fetching in error boundaries 5. Implement loading states — Use loading.tsx or Suspense boundaries 6. Prefer SWR/React Query for: real-time data, user interactions, background updates 7. Use Server Actions for: form submissions, mutations requiring cache revalidation
Constraints and Warnings
Critical Constraints
- Server Components cannot use hooks (
useState,useEffect) or client data fetching libraries - Client Components must include the
'use client'directive - The
fetchAPI in Next.js extends standard Web fetch with Next.js-specific caching options - Server Actions require
'use server'and can only be called from Client Components or form actions
Common Pitfalls
1. Fetching in loops — Avoid sequential fetches in Server Components; use parallel fetching 2. Cache poisoning — Do not use force-cache for user-specific or personalized data 3. Memory leaks — Clean up subscriptions in Client Components when using real-time data 4. 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.
Caching and Revalidation Strategies
Time-based Revalidation (ISR)
Use time-based revalidation when stale data is acceptable for a bounded period.
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: {
revalidate: 60,
},
});
return res.json();
}Choose the revalidation interval based on how often the data changes.
On-Demand Revalidation
Use Route Handlers or Server Actions with revalidateTag() or revalidatePath() when data should refresh immediately after a write.
// 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 });
}Keep invalidation tags stable and descriptive so read and write paths stay coordinated.
Tag Cached Data for Selective Invalidation
Attach cache tags when the same data source is read in multiple places.
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: {
tags: ['posts'],
revalidate: 3600,
},
});
return res.json();
}Use a small set of predictable tags instead of dynamically generating unnecessary tag variants.
Opt Out of Caching
Disable caching for highly dynamic or user-specific data.
async function getRealTimeData() {
const res = await fetch('https://api.example.com/data', {
cache: 'no-store',
});
return res.json();
}
export const dynamic = 'force-dynamic';Use no-store intentionally because it trades performance for freshness.
Cache Selection Checklist
- Use ISR when the page can tolerate bounded staleness.
- Use tags when a mutation needs to refresh multiple consumers.
- Use
no-storefor real-time, user-specific, or security-sensitive responses. - Avoid sharing cache entries across different user contexts.
Example: Blog Page 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: The page is cached and revalidated every hour.
Client-Side Data Fetching
Use client-side libraries when the component needs browser-driven refresh, optimistic interactions, or local cache coordination.
SWR Integration
Choose SWR for lightweight refresh and revalidation behavior.
Install with 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>
);
}Use SWR when the data model is simple and the main need is background refresh.
React Query Integration
Choose React Query when you need structured query keys, richer cache invalidation, or advanced mutation flows.
Install with 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>
);
}For hydration, optimistic updates, infinite queries, and invalidation strategies, see react-query.md.
Library Selection Guide
Prefer SWR when you need:
- Lightweight polling or refresh
- Minimal setup
- Simple cache behavior
Prefer React Query when you need:
- Query hydration from the server
- Rich mutation workflows
- Centralized invalidation rules
- Infinite or dependent queries
Example: 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: The component refreshes automatically while preserving a simple API.
Data Fetching Patterns
Server Component Fetching (Default)
Fetch directly in async Server Components when the data is needed for the first render and no browser-only state is required.
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>
);
}Use this as the baseline approach for content pages, dashboards, and route-level data requirements.
Parallel Data Fetching
Fetch multiple independent resources in parallel to reduce total waiting time.
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
}Use this pattern whenever requests do not depend on one another.
Sequential Data Fetching (When Dependencies Exist)
Fetch sequentially only when a later request requires data from an earlier one.
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 };
}Document the dependency explicitly so the slower path is intentional.
Pattern Selection
Use this checklist when choosing the fetch shape:
- Start with a Server Component when the page can render from server data alone.
- Use
Promise.all()for unrelated requests. - Keep sequential requests only for real dependencies.
- Split large pages into smaller async components when separate Suspense boundaries improve UX.
Example: Dashboard with Parallel Requests
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 requests execute concurrently, reducing total load time.
Error Boundaries Reference
Full ErrorBoundary implementations for Next.js data fetching error handling.
Basic ErrorBoundary
// 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;
}
}ErrorBoundary with Reset
Allows the user to retry after an error:
// app/components/ErrorBoundary.tsx
'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;
}
}Usage 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>
);
}Using ErrorBoundary with SWR/React Query
// app/components/SWRBoundary.tsx
'use client';
import { ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
export function SWRBoundary({ children, fallback = <div>Error loading data</div> }: Props) {
return <>{children}</>; // Wrap in parent ErrorBoundary in page component
}Error and Loading States
Treat loading and failure handling as part of the fetch architecture. Use boundaries and route-level files to keep degraded states predictable.
Creating Error Boundaries
Use an Error Boundary for client-side failures that should render a fallback instead of crashing the page.
// 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
Wrap interactive or client-driven fetch flows in a fallback UI.
// 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
Add a reset path when the user should be able to retry without leaving the page.
'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;
}
}Loading.tsx Pattern
Use loading.tsx for route-level loading UI that integrates with Suspense.
// 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
Use smaller Suspense boundaries when different areas of the page can load independently.
// 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>
);
}Selection Guide
- Use
loading.tsxfor route-level pending states. - Use Suspense boundaries to stream independent sections.
- Use Error Boundaries for recoverable UI failures.
- Add retry affordances when the user can recover without navigation.
React Query Advanced Patterns
Table of Contents
1. Prefetching and Hydration 2. Mutations and Optimistic Updates 3. Infinite Queries 4. Parallel Queries 5. Dependent Queries 6. Query Invalidation
Prefetching and Hydration
Server-Side Prefetching
Prefetch data on server for immediate hydration:
// app/posts/page.tsx
import {
dehydrate,
HydrationBoundary,
QueryClient,
} from '@tanstack/react-query';
import { Posts } from './Posts';
export default async function PostsPage() {
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: async () => {
const res = await fetch('https://api.example.com/posts');
return res.json();
},
});
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<Posts />
</HydrationBoundary>
);
}// app/posts/Posts.tsx
'use client';
import { useQuery } from '@tanstack/react-query';
export function Posts() {
const { data: posts } = useQuery({
queryKey: ['posts'],
queryFn: async () => {
const res = await fetch('https://api.example.com/posts');
return res.json();
},
});
return (
<ul>
{posts?.map((post: any) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}Mutations and Optimistic Updates
Basic Mutation
'use client';
import { useMutation, useQueryClient } from '@tanstack/react-query';
export function CreatePost() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: async (newPost: { title: string; content: string }) => {
const res = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newPost),
});
return res.json();
},
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
return (
<button
onClick={() =>
mutation.mutate({ title: 'New Post', content: 'Content here' })
}
disabled={mutation.isPending}
>
{mutation.isPending ? 'Creating...' : 'Create Post'}
</button>
);
}Optimistic Updates
'use client';
import { useMutation, useQueryClient } from '@tanstack/react-query';
interface Post {
id: string;
title: string;
content: string;
}
export function LikePost({ postId }: { postId: string }) {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: async () => {
const res = await fetch(`/api/posts/${postId}/like`, {
method: 'POST',
});
return res.json();
},
onMutate: async () => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['posts', postId] });
// Snapshot previous value
const previousPost = queryClient.getQueryData<Post>(['posts', postId]);
// Optimistically update
queryClient.setQueryData(['posts', postId], (old: Post | undefined) =>
old ? { ...old, likes: old.likes + 1 } : old
);
return { previousPost };
},
onError: (err, variables, context) => {
// Rollback on error
if (context?.previousPost) {
queryClient.setQueryData(['posts', postId], context.previousPost);
}
},
onSettled: () => {
// Always refetch after error or success
queryClient.invalidateQueries({ queryKey: ['posts', postId] });
},
});
return (
<button onClick={() => mutation.mutate()} disabled={mutation.isPending}>
Like
</button>
);
}Infinite Queries
Infinite Scroll
'use client';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useEffect } from 'react';
import { useInView } from 'react-intersection-observer';
interface Post {
id: string;
title: string;
}
interface PostsResponse {
posts: Post[];
nextCursor?: string;
}
export function InfinitePosts() {
const { ref, inView } = useInView();
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
status,
} = useInfiniteQuery<PostsResponse>({
queryKey: ['posts'],
queryFn: async ({ pageParam }) => {
const res = await fetch(`/api/posts?cursor=${pageParam || ''}`);
return res.json();
},
getNextPageParam: (lastPage) => lastPage.nextCursor,
initialPageParam: undefined as string | undefined,
});
useEffect(() => {
if (inView && hasNextPage) {
fetchNextPage();
}
}, [inView, hasNextPage, fetchNextPage]);
if (status === 'pending') return <div>Loading...</div>;
if (status === 'error') return <div>Error: {error.message}</div>;
return (
<div>
{data.pages.map((page, i) => (
<div key={i}>
{page.posts.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</div>
))}
<div ref={ref}>
{isFetchingNextPage && 'Loading more...'}
</div>
</div>
);
}Parallel Queries
'use client';
import { useQueries } from '@tanstack/react-query';
export function Dashboard() {
const results = useQueries({
queries: [
{
queryKey: ['user'],
queryFn: () => fetch('/api/user').then(r => r.json()),
},
{
queryKey: ['posts'],
queryFn: () => fetch('/api/posts').then(r => r.json()),
},
{
queryKey: ['analytics'],
queryFn: () => fetch('/api/analytics').then(r => r.json()),
},
],
});
const [user, posts, analytics] = results;
if (user.isLoading || posts.isLoading || analytics.isLoading) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Welcome {user.data?.name}</h1>
<p>Posts: {posts.data?.length}</p>
<p>Views: {analytics.data?.views}</p>
</div>
);
}Dependent Queries
'use client';
import { useQuery } from '@tanstack/react-query';
export function UserPosts({ userId }: { userId?: string }) {
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: async () => {
const res = await fetch(`/api/users/${userId}`);
return res.json();
},
enabled: !!userId, // Only run when userId exists
});
const { data: posts } = useQuery({
queryKey: ['posts', user?.id],
queryFn: async () => {
const res = await fetch(`/api/users/${user.id}/posts`);
return res.json();
},
enabled: !!user?.id, // Wait for user data
});
if (!userId) return <div>Select a user</div>;
if (!user) return <div>Loading user...</div>;
if (!posts) return <div>Loading posts...</div>;
return (
<ul>
{posts.map((post: any) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}Query Invalidation
Selective Invalidation
'use client';
import { useQueryClient } from '@tanstack/react-query';
export function RefreshControls() {
const queryClient = useQueryClient();
const refreshAll = () => {
queryClient.invalidateQueries();
};
const refreshPosts = () => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
};
const refreshCurrentPost = (postId: string) => {
queryClient.invalidateQueries({
queryKey: ['posts', postId],
exact: true,
});
};
return (
<div>
<button onClick={refreshAll}>Refresh All</button>
<button onClick={refreshPosts}>Refresh Posts</button>
</div>
);
}Background Refetching
'use client';
import { useQuery } from '@tanstack/react-query';
export function LiveData() {
const { data, isFetching, dataUpdatedAt } = useQuery({
queryKey: ['live-data'],
queryFn: async () => {
const res = await fetch('/api/live-data');
return res.json();
},
refetchInterval: 5000, // Refetch every 5 seconds
refetchIntervalInBackground: true,
staleTime: 0, // Always consider data stale
});
return (
<div>
<p>Data: {JSON.stringify(data)}</p>
{isFetching && <span>Updating...</span>}
<small>
Last updated: {new Date(dataUpdatedAt).toLocaleTimeString()}
</small>
</div>
);
}Server Actions Reference
Server Actions for mutations with error handling, form validation, and cache revalidation.
Basic Mutation
// 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();
}Form with Client-Side Error Handling
// app/posts/CreatePostForm.tsx
'use client';
import { createPost } from '../actions/posts';
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Creating...' : 'Create Post'}
</button>
);
}
export function CreatePostForm() {
return (
<form action={createPost}>
<input name="title" placeholder="Title" required />
<textarea name="content" placeholder="Content" required />
<SubmitButton />
</form>
);
}Form with useActionState for Error Handling
// app/actions/posts.ts
'use server';
'use server';
import { revalidateTag } from 'next/cache';
import { z } from 'zod';
const PostSchema = z.object({
title: z.string().min(1).max(100),
content: z.string().min(1).max(5000),
});
export type ActionState = {
errors?: { title?: string[]; content?: string[] };
message?: string;
};
export async function createPost(prevState: ActionState, formData: FormData) {
const validated = PostSchema.safeParse({
title: formData.get('title'),
content: formData.get('content'),
});
if (!validated.success) {
return {
errors: validated.error.flatten().fieldErrors,
message: 'Validation failed',
};
}
const response = await fetch('https://api.example.com/posts', {
method: 'POST',
body: JSON.stringify(validated.data),
});
if (!response.ok) {
return { message: 'Failed to create post' };
}
revalidateTag('posts');
return { message: 'Post created' };
}// app/posts/NewPostForm.tsx
'use client';
import { useActionState } from 'react';
import { createPost } from '../actions/posts';
export function NewPostForm() {
const [state, formAction, isPending] = useActionState(createPost, {});
return (
<form action={formAction}>
<input name="title" placeholder="Title" />
{state.errors?.title && <span>{state.errors.title[0]}</span>}
<textarea name="content" placeholder="Content" />
{state.errors?.content && <span>{state.errors.content[0]}</span>}
{state.message && <span>{state.message}</span>}
<button type="submit" disabled={isPending}>
{isPending ? 'Creating...' : 'Create Post'}
</button>
</form>
);
}Delete Action
// app/actions/posts.ts
export async function deletePost(postId: string) {
const response = await fetch(`/api/posts/${postId}`, {
method: 'DELETE',
});
if (!response.ok) {
throw new Error('Failed to delete post');
}
revalidateTag('posts');
}Related skills
Forks & variants (1)
Nextjs Data Fetching has 1 known copy in the catalog totaling 22 installs. They canonicalize to this original listing.
- giuseppe-trisciuoglio - 22 installs
How it compares
Use nextjs-data-fetching for App Router cache semantics; use generic React data skills when not on Next.js server caching APIs.
FAQ
What does nextjs-data-fetching do?
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 boundari
When should I use nextjs-data-fetching?
During build backend work for backend & apis.
Is nextjs-data-fetching safe to install?
Review the Security Audits panel on this listing before production use.