mcpField
N
nextjs-data-fetching

nextjs-data-fetching

github.com/giuseppe-trisciuoglio开发3653.7k 次安装7 周前更新
代码仓库

关于 nextjs-data-fetching

面向使用 Next.js App Router 的开发者的数据获取技能,涵盖服务端与客户端获取、缓存、重新验证、加载与错误状态,以及 Server Actions。

主要功能

  1. 01在 Server Components 中获取数据
  2. 02并行执行独立请求
  3. 03配置缓存与重新验证
  4. 04集成 SWR 或 React Query
  5. 05处理加载与错误状态
  6. 06使用 Server Actions 执行变更

适用场景

  1. 01构建按计划刷新的数据页面
  2. 02并发加载仪表盘数据
  3. 03添加客户端缓存与更新
  4. 04通过 Server Actions 提交表单并重新验证缓存

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill nextjs-data-fetching

安装后在 Claude Code 中输入/nextjs-data-fetching确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
caching-strategies.md
client-fetching.md
data-fetching.md
error-boundaries.md
error-loading-states.md
react-query.md
server-actions.md

详细介绍

Next.js 数据获取

概述

介绍 Next.js App Router 中的数据获取模式:服务端获取、SWR/React Query 集成、ISR、重新验证、错误边界和加载状态。

适用场景

  • 在 Next.js App Router 中实现数据获取
  • 在 Server Components 和 Client Components 之间进行选择
  • 设置 SWR 或 React Query,以实现客户端缓存
  • 配置 ISR、基于时间的重新验证或按需重新验证
  • 处理加载和错误状态
  • 使用 Server Actions 构建表单

说明

Server Component 中的数据获取

在异步 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>
  );
}
并行数据获取

对互不依赖的请求使用 Promise.all():

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 };
}
基于时间的重新验证(ISR)
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 60 } // Revalidate every 60 seconds
  });
  return res.json();
}
按需重新验证
// 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();
}

// Or:
export const dynamic = 'force-dynamic';

客户端数据获取

集成 SWR

安装: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

安装: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>
  );
}

有关变更、乐观更新、无限查询和高级模式,请参阅 react-query.md。

错误边界

使用错误边界包装客户端数据获取,以妥善处理失败情况:

有关完整的 ErrorBoundary 实现(基础版和带重置回调的版本)以及数据获取用法示例,请参阅 error-boundaries.md。

Server Actions

使用 Server Actions 执行变更并重新验证缓存:

有关完整示例(包括使用 useActionState 进行表单验证、错误处理和缓存失效),请参阅 server-actions.md。

加载状态

loading.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 边界
// 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>
  );
}

最佳实践

  1. 默认使用 Server Components — 在 Server Components 中获取数据,以提升性能
  2. 使用并行获取 — 对互不依赖的请求使用 Promise.all(),以降低延迟
  3. 选择合适的缓存策略:
    • 静态数据:使用较长的重新验证间隔
    • 动态数据:使用较短的重新验证间隔,或设置 cache: 'no-store'
    • 用户专属数据:使用动态渲染
  4. 妥善处理错误 — 使用错误边界包装客户端数据获取
  5. 实现加载状态 — 使用 loading.tsx 或 Suspense 边界
  6. 优先在以下场景使用 SWR/React Query:实时数据、用户交互、后台更新
  7. 在以下场景使用 Server Actions:表单提交、需要重新验证缓存的变更

限制与警告

关键限制
  • Server Components 不能使用 hooks(useState、useEffect)或客户端数据获取库
  • Client Components 必须包含 'use client' 指令
  • Next.js 中的 fetch API 在标准 Web fetch 的基础上扩展了 Next.js 专属缓存选项
  • Server Actions 需要 'use server',且只能从 Client Components 或表单操作中调用
常见问题
  1. 在循环中获取数据 — 避免在 Server Components 中顺序获取数据;请使用并行获取
  2. 缓存污染 — 不要对用户专属或个性化数据使用 force-cache
  3. 内存泄漏 — 使用实时数据时,请在 Client Components 中清理订阅
  4. 水合不匹配 — 使用 React Query 水合时,确保服务端和客户端渲染相同的初始状态

示例

示例 1:使用 ISR 的博客

输入: 创建一个博客页面,获取文章并每小时更新一次。

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

输出: 页面在构建时静态生成,并每小时重新验证一次。

示例 2:并行获取数据的仪表盘

输入: 构建一个仪表盘,并行显示用户资料、统计数据和最近活动。

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

输出: 三个请求同时执行,最大限度地缩短总加载时间。

评论

支持 Markdown