mcpField
N
nextjs-data-fetching

nextjs-data-fetching

github.com/josiahsiegel开发5976 次安装20 周前更新
代码仓库

关于 nextjs-data-fetching

面向 Next.js App Router 开发者的数据加载参考技能,涵盖服务端与客户端获取数据、缓存控制、流式渲染和 Route Handler。

主要功能

  1. 01服务端与客户端数据获取
  2. 02并行与顺序请求
  3. 03缓存及重新验证设置
  4. 04使用 Suspense 流式渲染
  5. 05Route Handler 示例

适用场景

  1. 01在 Server Components 中加载数据
  2. 02并发获取互不依赖的数据
  3. 03逐步流式呈现仪表板内容
  4. 04通过 Route Handler 创建 API 端点

安装

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

$npx skills add https://github.com/josiahsiegel/claude-plugin-marketplace --skill nextjs-data-fetching

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

详细介绍

快速参考

模式代码用途
服务端获取await fetch(url)默认缓存
不缓存{ cache: 'no-store' }始终获取最新数据
重新验证{ next: { revalidate: 60 } }按时间刷新
标签{ next: { tags: ['posts'] } }基于标签失效
配置值效果
dynamic'force-dynamic'始终使用 SSR
revalidate60每 60s 进行一次 ISR
fetchCache'force-no-store'不缓存
客户端库Hook使用场景
SWRuseSWR(key, fetcher)简单的客户端数据获取
TanStack QueryuseQuery({ queryKey, queryFn })复杂状态和变更

何时使用此技能

适用于数据加载模式:

  • 在 Server Components 中获取数据
  • 使用 Promise.all 设置并行数据获取
  • 使用 Suspense 边界实现流式传输
  • 使用 Route Handlers 创建 API 路由
  • 客户端数据获取和变更

相关技能:

  • 关于缓存策略:参见 nextjs-caching
  • 关于 Server Actions(变更):参见 nextjs-server-actions
  • 关于 App Router 基础知识:参见 nextjs-app-router

Next.js 数据获取

Server Components 数据获取

基本数据获取
// app/posts/page.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: Post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}
直接访问数据库
// app/users/page.tsx
import { db } from '@/lib/db';

export default async function UsersPage() {
  // Direct database query - no API needed
  const users = await db.users.findMany({
    orderBy: { createdAt: 'desc' },
    take: 10,
  });

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}
并行数据获取
// app/dashboard/page.tsx
async function getUser() {
  const res = await fetch('https://api.example.com/user');
  return res.json();
}

async function getPosts() {
  const res = await fetch('https://api.example.com/posts');
  return res.json();
}

async function getAnalytics() {
  const res = await fetch('https://api.example.com/analytics');
  return res.json();
}

export default async function DashboardPage() {
  // Fetch all data in parallel
  const [user, posts, analytics] = await Promise.all([
    getUser(),
    getPosts(),
    getAnalytics(),
  ]);

  return (
    <div>
      <UserCard user={user} />
      <PostsList posts={posts} />
      <AnalyticsChart data={analytics} />
    </div>
  );
}
顺序数据获取
// When data depends on previous request
async function getUser(userId: string) {
  const res = await fetch(`https://api.example.com/users/${userId}`);
  return res.json();
}

async function getUserPosts(userId: string) {
  const res = await fetch(`https://api.example.com/users/${userId}/posts`);
  return res.json();
}

export default async function UserPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;

  // Sequential: user first, then posts
  const user = await getUser(id);
  const posts = await getUserPosts(user.id);

  return (
    <div>
      <h1>{user.name}</h1>
      <PostsList posts={posts} />
    </div>
  );
}

缓存和重新验证

缓存行为
// Cached by default (equivalent to cache: 'force-cache')
const data = await fetch('https://api.example.com/data');

// Opt out of caching
const data = await fetch('https://api.example.com/data', {
  cache: 'no-store',
});

// Time-based revalidation
const data = await fetch('https://api.example.com/data', {
  next: { revalidate: 3600 }, // Revalidate every hour
});

// Tag-based revalidation
const data = await fetch('https://api.example.com/posts', {
  next: { tags: ['posts'] },
});
路由段配置
// app/posts/page.tsx
export const dynamic = 'force-dynamic'; // Always dynamic
// or
export const revalidate = 60; // Revalidate every 60 seconds
// or
export const fetchCache = 'force-no-store'; // Don't cache any fetches
重新验证数据
// app/actions.ts
'use server';

import { revalidatePath, revalidateTag } from 'next/cache';

export async function createPost(formData: FormData) {
  await db.posts.create({
    data: { title: formData.get('title') as string },
  });

  // Revalidate specific path
  revalidatePath('/posts');

  // Or revalidate by tag
  revalidateTag('posts');
}

使用 Suspense 进行流式传输

组件级流式传输
// app/dashboard/page.tsx
import { Suspense } from 'react';

async function SlowComponent() {
  const data = await fetch('https://api.example.com/slow-data');
  return <div>{/* render data */}</div>;
}

async function FastComponent() {
  const data = await fetch('https://api.example.com/fast-data');
  return <div>{/* render data */}</div>;
}

export default function DashboardPage() {
  return (
    <div>
      {/* Fast component renders immediately */}
      <Suspense fallback={<FastSkeleton />}>
        <FastComponent />
      </Suspense>

      {/* Slow component streams in when ready */}
      <Suspense fallback={<SlowSkeleton />}>
        <SlowComponent />
      </Suspense>
    </div>
  );
}
嵌套 Suspense 边界
// app/page.tsx
import { Suspense } from 'react';

export default function Page() {
  return (
    <Suspense fallback={<PageSkeleton />}>
      <Header />
      <Suspense fallback={<SidebarSkeleton />}>
        <Sidebar />
      </Suspense>
      <Suspense fallback={<ContentSkeleton />}>
        <MainContent />
        <Suspense fallback={<CommentsSkeleton />}>
          <Comments />
        </Suspense>
      </Suspense>
    </Suspense>
  );
}

客户端数据获取

SWR
'use client';

import useSWR from 'swr';

const fetcher = (url: string) => fetch(url).then((res) => res.json());

export function UserProfile({ userId }: { userId: string }) {
  const { data, error, isLoading, mutate } = useSWR(
    `/api/users/${userId}`,
    fetcher
  );

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error loading user</div>;

  return (
    <div>
      <h1>{data.name}</h1>
      <button onClick={() => mutate()}>Refresh</button>
    </div>
  );
}
TanStack Query
// providers/query-provider.tsx
'use client';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useState } from 'react';

export function QueryProvider({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(
    () =>
      new QueryClient({
        defaultOptions: {
          queries: {
            staleTime: 60 * 1000,
          },
        },
      })
  );

  return (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );
}
// components/Posts.tsx
'use client';

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

async function fetchPosts() {
  const res = await fetch('/api/posts');
  return res.json();
}

async function createPost(data: { title: string }) {
  const res = await fetch('/api/posts', {
    method: 'POST',
    body: JSON.stringify(data),
  });
  return res.json();
}

export function Posts() {
  const queryClient = useQueryClient();

  const { data: posts, isLoading } = useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
  });

  const mutation = useMutation({
    mutationFn: createPost,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['posts'] });
    },
  });

  if (isLoading) return <div>Loading...</div>;

  return (
    <div>
      <ul>
        {posts?.map((post: Post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
      <button onClick={() => mutation.mutate({ title: 'New Post' })}>
        Add Post
      </button>
    </div>
  );
}

Route Handlers(API 路由)

基本 Route Handler
// app/api/posts/route.ts
import { NextResponse } from 'next/server';

export async function GET() {
  const posts = await db.posts.findMany();
  return NextResponse.json(posts);
}

export async function POST(request: Request) {
  const body = await request.json();
  const post = await db.posts.create({ data: body });
  return NextResponse.json(post, { status: 201 });
}
动态 Route Handler
// app/api/posts/[id]/route.ts
import { NextResponse } from 'next/server';

interface RouteParams {
  params: Promise<{ id: string }>;
}

export async function GET(request: Request, { params }: RouteParams) {
  const { id } = await params;
  const post = await db.posts.findUnique({ where: { id } });

  if (!post) {
    return NextResponse.json({ error: 'Not found' }, { status: 404 });
  }

  return NextResponse.json(post);
}

export async function PUT(request: Request, { params }: RouteParams) {
  const { id } = await params;
  const body = await request.json();
  const post = await db.posts.update({
    where: { id },
    data: body,
  });
  return NextResponse.json(post);
}

export async function DELETE(request: Request, { params }: RouteParams) {
  const { id } = await params;
  await db.posts.delete({ where: { id } });
  return new NextResponse(null, { status: 204 });
}
请求辅助工具
// app/api/search/route.ts
import { NextResponse } from 'next/server';

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const query = searchParams.get('q');
  const page = parseInt(searchParams.get('page') || '1');

  const results = await search(query, page);

  return NextResponse.json(results);
}
请求头和 Cookie
// app/api/auth/route.ts
import { NextResponse } from 'next/server';
import { cookies, headers } from 'next/headers';

export async function GET() {
  const cookieStore = await cookies();
  const token = cookieStore.get('token');

  const headersList = await headers();
  const authorization = headersList.get('authorization');

  // Set cookie in response
  const response = NextResponse.json({ success: true });
  response.cookies.set('session', 'value', {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 60 * 60 * 24, // 1 day
  });

  return response;
}

静态生成

generateStaticParams
// app/posts/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await db.posts.findMany({ select: { slug: true } });

  return posts.map((post) => ({
    slug: post.slug,
  }));
}

export default async function PostPage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await db.posts.findUnique({ where: { slug } });
  return <article>{post?.content}</article>;
}
动态参数行为
// Allow dynamic paths beyond generateStaticParams
export const dynamicParams = true; // default

// 404 for paths not in generateStaticParams
export const dynamicParams = false;

错误处理

Fetch 错误处理
async function getData() {
  const res = await fetch('https://api.example.com/data');

  if (!res.ok) {
    throw new Error('Failed to fetch data');
  }

  return res.json();
}

export default async function Page() {
  try {
    const data = await getData();
    return <div>{/* render data */}</div>;
  } catch (error) {
    return <div>Error loading data</div>;
  }
}
使用 error.tsx
// app/posts/error.tsx
'use client';

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div>
      <h2>Error loading posts</h2>
      <p>{error.message}</p>
      <button onClick={() => reset()}>Try again</button>
    </div>
  );
}

最佳实践

实践描述
在 Server Components 中获取数据尽可能避免客户端数据获取
使用并行获取对独立数据使用 Promise.all
实现流式传输使用 Suspense 实现渐进式加载
适当缓存使用 revalidate 或 tags 获取最新数据
处理错误使用 error.tsx 边界
为数据添加类型使用 TypeScript 定义 API 响应类型

评论

支持 Markdown