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:
cache: 'force-cache'(default): Reuse cached results as much as possible, suitable for infrequently changing content.cache: 'no-store': Fetch from the origin server every time, suitable for highly real-time or user-private data (still needs auth).
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).
- Use the
'use server'directive at the top of a file or function to mark that module or function as server-side code. - Inline: Define an async function directly in a Server Component file and add
'use server'(function-body level). - Separate file: e.g.,
app/actions.tswith'use server'on the first line, exporting multiple actions for forms or client components to import.
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).
- useFormStatus (from
react-dom): Readpendinginside a form's child component to disable buttons or show "Submitting...". - useActionState (React 19, from
react): Wraps a Server Action into a function that receives the previous return state, suitable for error messages and validation result display (successor touseFormState).
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: Use
Promise.all(orPromise.allSettled) to fire simultaneously, reducing total wait time. - Sequential: When the next step depends on previous results, use consecutive
awaits. - Preloading & Deduplication: React's
cache(fn)wraps data fetching functions, reusing the same request result across multiple calls (works with RSC rendering deduplication).
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:
revalidatePath('/path'): Invalidate by path; the next visit to that route will re-fetch data.revalidateTag('tag'): Invalidate all cache entries that share the same tag on theirfetch.
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.