← Back to Index

Chapter 4: Data Fetching

fetch, Server Actions & Caching Strategies

1. Server-Side Data Fetching

In App Router, default-exported page and layout components can be async Server Components. This means you can use async/await directly at the component top level to fetch data, just like writing regular async functions, without needing to put data fetching inside useEffect.

Next.js extends the browser's native fetch: in addition to standard parameters, it supports cache, next.revalidate, next.tags and other options to align with full-route caching and on-demand invalidation strategies.

The following example fetches JSON and renders a list in an async page component (adjust paths for your project):

type Post = { id: number; title: string };

async function getPosts(): Promise<Post[]> {
  const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=5", {
    cache: "force-cache",
  });
  if (!res.ok) throw new Error("Failed to fetch posts");
  return res.json();
}

export default async function Page() {
  const posts = await getPosts();

  return (
    <main className="p-8">
      <h1 className="text-2xl font-bold">文章列表</h1>
      <ul className="mt-4 space-y-2">
        {posts.map((post) => (
          <li key={post.id} className="border-b pb-2">
            <span className="font-mono text-gray-500">#{post.id}</span> {post.title}
          </li>
        ))}
      </ul>
    </main>
  );
}

💡 Key Point

In Server Components, you can directly await fetch(...) — data is resolved on the server and the HTML/RSC payload is sent to the client, no need for useEffect and client-side re-fetching (unless you need browser-side interaction or subscriptions).

For those with React SPA experience

In pure CSR applications, the common pattern is useEffect + fetch; in Next.js App Router, prefer putting reads that can be done statically or server-side in Server Components to reduce client JS and hydration costs.

2. Caching Strategies

The second parameter of fetch controls data caching behavior:

You can also use next: { revalidate: number } for time-based revalidation (ISR-style): after the specified seconds, the next visit triggers background revalidation.

Three typical patterns:

// 1) 默认:静态缓存(构建期或首次请求后复用,具体行为见官方 Data Cache 说明)
await fetch("https://api.example.com/items", { cache: "force-cache" });

// 2) 禁用缓存:始终动态获取
await fetch("https://api.example.com/me", { cache: "no-store" });

// 3) 时间窗口 revalidate:每 60 秒最多后台刷新一次
await fetch("https://api.example.com/feed", {
  next: { revalidate: 60 },
});

When debugging with Route Handlers or external tools, use curl for quick API verification (unrelated to Next caching, just for debugging):

curl -sS "https://jsonplaceholder.typicode.com/posts/1" | head -c 200

POST request bodies commonly use JSON format, e.g., creating a resource:

{
  "title": "Hello Next.js 15",
  "body": "Server Actions 与 fetch 缓存",
  "userId": 1
}
Method Typical Scenario Behavior Summary
cache: 'force-cache' Blog posts, documentation site public content Hit Data Cache when possible, reducing upstream load.
cache: 'no-store' Dashboards, personalized APIs No persistent caching, fetches fresh data every time.
next: { revalidate: n } News feeds, prices needing periodic refresh Reusable within n-second window, updates in background after expiry.

3. Server Actions

Server Actions are async functions that execute on the server, used for form submissions, database writes, calling internal APIs, etc., without needing to expose separate HTTP routes (the compiler generates secure endpoints).

Separate file example (src/app/actions.ts):

'use server';

export async function createPost(formData: FormData) {
  const title = String(formData.get("title") ?? "");
  const body = String(formData.get("body") ?? "");
  const res = await fetch("https://jsonplaceholder.typicode.com/posts", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ title, body, userId: 1 }),
  });
  if (!res.ok) {
    throw new Error("createPost failed");
  }
  return res.json();
}

Inline definition in a Server Component bound to a form (simplified demo):

export default async function Page() {
  async function submit(formData: FormData) {
    "use server";
    const name = String(formData.get("name") ?? "");
    // 此处可写入数据库或调用内部服务
    console.log("server received:", name);
  }

  return (
    <form action={submit}>
      <input name="name" className="border px-2" />
      <button type="submit">提交</button>
    </form>
  );
}

4. Forms & Server Actions

The native <form>'s action can be directly bound to a Server Action: on submission, it executes server-side, with native progressive enhancement support (works even without JS).

Complete example: Server Action returning messages + client showing pending and state (split into Client sub-component for hooks rules):

// app/actions.ts
'use server';

export type FormState = { ok: boolean; message: string };

export async function subscribe(
  _prevState: FormState,
  formData: FormData
): Promise<FormState> {
  const email = String(formData.get("email") ?? "").trim();
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
    return { ok: false, message: "邮箱格式不正确" };
  }
  // 模拟写入
  return { ok: true, message: "已订阅:" + email };
}
// app/ui/subscribe-form.tsx
'use client';

import { useFormStatus } from 'react-dom';
import { useActionState } from 'react';
import { subscribe, type FormState } from '@/app/actions';

function SubmitButton() {
  const { pending } = useFormStatus();
  return (
    <button
      type="submit"
      disabled={pending}
      className="rounded bg-sky-600 px-4 py-2 text-white disabled:opacity-50"
    >
      {pending ? '提交中…' : '订阅'}
    </button>
  );
}

const initialState: FormState = { ok: false, message: '' };

export function SubscribeForm() {
  const [state, formAction] = useActionState(subscribe, initialState);

  return (
    <form action={formAction} className="space-y-2">
      <input
        name="email"
        type="email"
        placeholder="you@example.com"
        className="w-full rounded border px-3 py-2"
        required
      />
      <SubmitButton />
      {state.message ? (
        <p className={state.ok ? 'text-green-600' : 'text-red-600'}>
          {state.message}
        </p>
      ) : null}
    </form>
  );
}
// app/page.tsx(Server Component 中引用客户端表单)
import { SubscribeForm } from '@/app/ui/subscribe-form';

export default function Page() {
  return (
    <main className="p-8">
      <h1 className="text-xl font-bold">邮件订阅</h1>
      <SubscribeForm />
    </main>
  );
}

5. Data Fetching Patterns

When a page needs multiple APIs, first analyze dependencies: independent requests should be parallel; dependent ones should be sequential.

Parallel vs Sequential:

import { cache } from 'react';

const getUser = cache(async (id: string) => {
  const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`, {
    cache: 'no-store',
  });
  return res.json();
});

export default async function DashboardPage() {
  // 并行:用户与文章列表互不依赖
  const [user, posts] = await Promise.all([
    getUser('1'),
    fetch('https://jsonplaceholder.typicode.com/posts?userId=1', {
      cache: 'no-store',
    }).then((r) => r.json()),
  ]);

  // 顺序:先取 post 再取评论(示例)
  const firstPost = posts[0];
  const comments = firstPost
    ? await fetch(
        `https://jsonplaceholder.typicode.com/posts/${firstPost.id}/comments`,
        { cache: 'no-store' }
      ).then((r) => r.json())
    : [];

  return (
    <section className="p-8">
      <h1 className="text-2xl font-bold">{user.name}</h1>
      <p className="text-gray-600">{user.email}</p>
      <h2 className="mt-6 font-semibold">评论数:{comments.length}</h2>
    </section>
  );
}

💡 Key Point

Always fetch independent data in parallel, avoiding unnecessary sequential await waterfalls in Server Components; for repeated pure function calls, use cache() to consolidate.

6. On-Demand Revalidation

When a Server Action or Route Handler writes new data, you may want to immediately invalidate certain caches rather than waiting for the revalidate time window. Next.js provides:

Calling revalidatePath after a Server Action update:

'use server';

import { revalidatePath } from 'next/cache';

export async function addItem(formData: FormData) {
  const title = String(formData.get('title') ?? '');
  // await db.item.create({ data: { title } })
  revalidatePath('/items');
}

Tagging a fetch and using revalidateTag after changes:

// 读取侧(Server Component 或共享数据模块)
const res = await fetch('https://api.example.com/posts', {
  next: { tags: ['posts'] },
});
const posts = await res.json();
'use server';

import { revalidateTag } from 'next/cache';

export async function refreshPosts() {
  revalidateTag('posts');
}

Strategy tip: Use tags for list page cache aggregation; detail pages can use revalidatePath('/posts/[id]') for precise refresh; maintain consistent conventions across your project.

7. Chapter Summary

Server-Side Fetching

Use async/await with the extended fetch directly in Server Components; understand default caching, no-store, and revalidate.

Server Actions

Declare server functions with 'use server'; bind to form action=; put complex logic in a separate actions.ts.

Form UX

useFormStatus handles pending state; useActionState handles previous return values and validation errors.

Patterns & Invalidation

Promise.all for parallel fetching, cache() for deduplication; after writes, use revalidatePath / revalidateTag for on-demand refresh.