nextjs-data-fetching

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Next.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
revalidateTag
or
revalidatePath
:
tsx
// 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();
}
使用路由处理器配合
revalidateTag
revalidatePath
tsx
// 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 swr
tsx
'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 swr
tsx
'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-query
Setup 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

最佳实践

  1. Default to Server Components - Fetch data in Server Components when possible for better performance
  2. Use parallel fetching - Use
    Promise.all()
    for independent data requests
  3. Choose appropriate caching:
    • Static data: Long revalidation intervals or no revalidation
    • Dynamic data: Short revalidation or
      cache: 'no-store'
    • User-specific: Use dynamic rendering
  4. Handle errors gracefully - Wrap client data fetching in error boundaries
  5. Use loading states - Implement
    loading.tsx
    or Suspense boundaries
  6. Prefer SWR/React Query for:
    • Real-time data
    • User interactions requiring immediate feedback
    • Data that needs background updates
  7. Use Server Actions for:
    • Form submissions
    • Mutations that need to revalidate cache
    • Operations requiring server-side logic
  1. 优先使用Server Components - 尽可能在Server Components中获取数据以提升性能
  2. 使用并行获取 - 对独立的数据请求使用
    Promise.all()
  3. 选择合适的缓存策略
    • 静态数据:较长的重新验证间隔或不设置重新验证
    • 动态数据:较短的重新验证间隔或使用
      cache: 'no-store'
    • 用户专属数据:使用动态渲染
  4. 优雅处理错误 - 在客户端数据获取外层包裹错误边界
  5. 使用加载状态 - 实现
    loading.tsx
    或Suspense边界
  6. 优先选择SWR/React Query的场景
    • 实时数据
    • 需要即时反馈的用户交互
    • 需要后台更新的数据
  7. 优先选择Server Actions的场景
    • 表单提交
    • 需要重新验证缓存的数据变更操作
    • 需要服务端逻辑的操作

Constraints and Warnings

约束与注意事项

Critical Constraints

关键约束

  • Server Components cannot use hooks like
    useState
    ,
    useEffect
    , or data fetching libraries (SWR, React Query)
  • Client Components must include the
    'use client'
    directive
  • The
    fetch
    API in Next.js extends the standard Web API with Next.js-specific caching options
  • Server Actions require the
    'use server'
    directive and can only be called from Client Components or form actions
  • Server Components无法使用
    useState
    useEffect
    等钩子或数据获取库(SWR、React Query)
  • Client Components必须包含
    'use client'
    指令
  • Next.js中的
    fetch
    API扩展了标准Web API,增加了Next.js专属的缓存选项
  • Server Actions需要
    'use server'
    指令,且只能从Client Components或表单动作中调用

Common Pitfalls

常见陷阱

  1. Fetching in loops: Avoid fetching data inside loops in Server Components; use parallel fetching instead
  2. Cache poisoning: Be careful with
    cache: 'force-cache'
    for user-specific data
  3. Memory leaks: Always clean up subscriptions in Client Components when using real-time data
  4. Hydration mismatches: Ensure server and client render the same initial state when using React Query hydration
  1. 循环内获取数据:避免在Server Components的循环中获取数据;改用并行获取
  2. 缓存污染:针对用户专属数据使用
    cache: 'force-cache'
    时需谨慎
  3. 内存泄漏:在使用实时数据时,务必在Client Components中清理订阅
  4. ** hydration不匹配**:使用React Query的hydration功能时,确保服务端与客户端渲染的初始状态一致

Decision Matrix

决策矩阵

ScenarioSolution
Static content, infrequent updatesServer Component + ISR
Dynamic content, user-specificServer Component +
cache: 'no-store'
Real-time updatesClient Component + SWR/React Query
User interactionsClient Component + mutation library
Mixed requirementsServer for initial, Client for updates
场景解决方案
静态内容,更新频率低Server Component + ISR
动态内容,用户专属Server Component +
cache: 'no-store'
实时更新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提交,成功后缓存会被失效。