nextjs-data-fetching by giuseppe-trisciuoglio/developer-kit
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill nextjs-data-fetching此技能为 Next.js App Router 应用程序提供了全面的数据获取模式。它涵盖了服务器端获取、客户端库集成、缓存策略、错误处理和加载状态。
在以下场景中使用此技能:
直接在异步服务器组件中获取数据:
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>
);
}
并行获取多个资源:
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();
// 渲染仪表板
}
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
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 // 每 60 秒重新验证一次
}
});
return res.json();
}
使用带有 revalidateTag 或 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 });
}
为缓存数据打标签以便选择性重新验证:
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: {
tags: ['posts'],
revalidate: 3600
}
});
return res.json();
}
// 动态渲染(无缓存)
async function getRealTimeData() {
const res = await fetch('https://api.example.com/data', {
cache: 'no-store'
});
return res.json();
}
// 或者使用动态导出
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
设置 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 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>
);
}
查看 react-query.md 了解高级模式。
// 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.tsx 或 Suspense 边界
在以下场景中优先使用 SWR/React Query:
在以下场景中使用服务器操作:
useState、useEffect 这样的钩子或数据获取库(SWR、React Query)'use client' 指令fetch API 扩展了标准的 Web API,提供了 Next.js 特定的缓存选项'use server' 指令,并且只能从客户端组件或表单操作中调用cache: 'force-cache' 时要小心| 场景 | 解决方案 |
|---|---|
| 静态内容,不频繁更新 | 服务器组件 + ISR |
| 动态内容,用户特定 | 服务器组件 + cache: 'no-store' |
| 实时更新 | 客户端组件 + SWR/React Query |
| 用户交互 | 客户端组件 + 变更库 |
| 混合需求 | 服务器用于初始加载,客户端用于更新 |
输入: 创建一个博客页面,获取文章并每小时更新一次。
// 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>
);
}
输出: 所有三个请求并发执行,减少了总加载时间。
输入: 显示每 5 秒更新一次的实时加密货币价格。
// 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>
);
}
输出: 表单通过服务器操作提交,成功后缓存失效。
每周安装量
217
代码仓库
GitHub 星标数
173
首次出现
2026年2月20日
安全审计
安装于
codex193
gemini-cli191
github-copilot188
cursor188
kimi-cli186
opencode186
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.
Use this skill for:
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>
);
}
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
}
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();
}
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();
}
// 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';
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>
);
}
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.
// 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>
);
}
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 :
cache: 'no-store'Handle errors gracefully - Wrap client data fetching in error boundaries
Use loading states - Implement loading.tsx or Suspense boundaries
Prefer SWR/React Query for :
Use Server Actions for :
useState, useEffect, or data fetching libraries (SWR, React Query)'use client' directivefetch API in Next.js extends the standard Web API with Next.js-specific caching options'use server' directive and can only be called from Client Components or form actionscache: 'force-cache' for user-specific data| 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 |
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.
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.
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.
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.
Weekly Installs
217
Repository
GitHub Stars
173
First Seen
Feb 20, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex193
gemini-cli191
github-copilot188
cursor188
kimi-cli186
opencode186
Flutter应用架构设计指南:分层结构、数据层实现与最佳实践
4,000 周安装