nextjs-data-fetching
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseNext.js Data Fetching
Next.js 数据获取
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.
本技能提供Next.js App Router应用中全面的数据获取模式,涵盖服务端获取、客户端库集成、缓存策略、错误处理以及加载状态管理。
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
在以下场景中使用本技能:
- 在Next.js App Router中实现数据获取
- 为数据获取选择Server Components或Client Components
- 配置SWR或React Query集成
- 实现并行数据获取模式
- 配置ISR与重新验证策略
- 为数据获取创建错误边界
Instructions
操作指南
Server Component Fetching (Default)
Server Component数据获取(默认方式)
Fetch directly in async Server Components:
tsx
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>
);
}在异步Server Components中直接获取数据:
tsx
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:
tsx
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
}并行获取多个资源:
tsx
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)
串行数据获取(存在依赖关系时)
tsx
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 };
}tsx
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)
基于时间的重新验证(ISR)
tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: {
revalidate: 60 // Revalidate every 60 seconds
}
});
return res.json();
}tsx
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 or :
revalidateTagrevalidatePathtsx
// 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:
tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: {
tags: ['posts'],
revalidate: 3600
}
});
return res.json();
}使用路由处理器配合或:
revalidateTagrevalidatePathtsx
// 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 });
}为缓存数据添加标签以实现选择性重新验证:
tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: {
tags: ['posts'],
revalidate: 3600
}
});
return res.json();
}Opt-out of Caching
禁用缓存
tsx
// 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';tsx
// 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
SWR集成
Install:
npm install swrtsx
'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 swrtsx
'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
React Query集成
Install:
npm install @tanstack/react-querySetup provider:
tsx
// 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:
tsx
'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.
安装:
npm install @tanstack/react-query设置Provider:
tsx
// 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>
);
}在组件中使用:
tsx
'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了解高级模式。
Error Boundaries
错误边界
Creating Error Boundaries
创建错误边界
tsx
// 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;
}
}tsx
// 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
结合数据获取使用错误边界
tsx
// 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>
);
}tsx
// 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
带重置功能的错误边界
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;
}
}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;
}
}Server Actions for Mutations
用于数据变更的Server Actions
tsx
// 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();
}tsx
// 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>
);
}tsx
// 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();
}tsx
// 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
Loading.tsx模式
tsx
// 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>
);
}tsx
// 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
Suspense边界
tsx
// 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>
);
}tsx
// 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>
);
}Best Practices
最佳实践
-
Default to Server Components - Fetch data in Server Components when possible for better performance
-
Use parallel fetching - Usefor independent data requests
Promise.all() -
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 - Implementor Suspense boundaries
loading.tsx -
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
-
优先使用Server Components - 尽可能在Server Components中获取数据以提升性能
-
使用并行获取 - 对独立的数据请求使用
Promise.all() -
选择合适的缓存策略:
- 静态数据:较长的重新验证间隔或不设置重新验证
- 动态数据:较短的重新验证间隔或使用
cache: 'no-store' - 用户专属数据:使用动态渲染
-
优雅处理错误 - 在客户端数据获取外层包裹错误边界
-
使用加载状态 - 实现或Suspense边界
loading.tsx -
优先选择SWR/React Query的场景:
- 实时数据
- 需要即时反馈的用户交互
- 需要后台更新的数据
-
优先选择Server Actions的场景:
- 表单提交
- 需要重新验证缓存的数据变更操作
- 需要服务端逻辑的操作
Constraints and Warnings
约束与注意事项
Critical Constraints
关键约束
- Server Components cannot use hooks like ,
useState, or data fetching libraries (SWR, React Query)useEffect - Client Components must include the directive
'use client' - The API in Next.js extends the standard Web API with Next.js-specific caching options
fetch - Server Actions require the directive and can only be called from Client Components or form actions
'use server'
- Server Components无法使用、
useState等钩子或数据获取库(SWR、React Query)useEffect - Client Components必须包含指令
'use client' - Next.js中的API扩展了标准Web API,增加了Next.js专属的缓存选项
fetch - Server Actions需要指令,且只能从Client Components或表单动作中调用
'use server'
Common Pitfalls
常见陷阱
- Fetching in loops: Avoid fetching data inside loops in Server Components; use parallel fetching instead
- Cache poisoning: Be careful with for user-specific data
cache: 'force-cache' - 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
- 循环内获取数据:避免在Server Components的循环中获取数据;改用并行获取
- 缓存污染:针对用户专属数据使用时需谨慎
cache: 'force-cache' - 内存泄漏:在使用实时数据时,务必在Client Components中清理订阅
- ** hydration不匹配**:使用React Query的hydration功能时,确保服务端与客户端渲染的初始状态一致
Decision Matrix
决策矩阵
| 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 |
| 场景 | 解决方案 |
|---|---|
| 静态内容,更新频率低 | Server Component + ISR |
| 动态内容,用户专属 | Server Component + |
| 实时更新 | Client Component + SWR/React Query |
| 用户交互 | Client Component + 数据变更库 |
| 混合需求 | 服务端处理初始加载,客户端处理更新 |
Examples
示例
Example 1: Basic Server Component with ISR
示例1:基础Server Component搭配ISR
Input: Create a blog page that fetches posts and updates every hour.
tsx
// 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.
需求:创建一个博客页面,获取文章数据并每小时更新一次。
tsx
// 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>
);
}效果:页面在构建时静态生成,每小时自动重新验证更新。
Example 2: Parallel Data Fetching for Dashboard
示例2:仪表盘的并行数据获取
Input: Build a dashboard showing user profile, stats, and recent activity.
tsx
// 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.
需求:构建一个展示用户资料、统计数据与近期活动的仪表盘。
tsx
// 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>
);
}效果:三个请求同时执行,减少整体加载时间。
Example 3: Real-time Data with SWR
示例3:使用SWR实现实时数据
Input: Display live cryptocurrency prices that update every 5 seconds.
tsx
// 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.
需求:展示每5秒更新一次的实时加密货币价格。
tsx
// 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>
);
}效果:组件展示实时更新的价格数据,并自动刷新。
Example 4: Form Submission with Server Action
示例4:使用Server Action提交表单
Input: Create a contact form that submits data and refreshes the cache.
tsx
// 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');
}tsx
// 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.
需求:创建一个联系表单,提交数据并刷新缓存。
tsx
// 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');
}tsx
// 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>
);
}效果:表单通过Server Action提交,成功后缓存会被失效。