Server Component 可以渲染 Client Component 并传可序列化的 props。反过来需要 children 模式,把 server 渲染好的内容作为 children prop 喂给 client 包装器。
// server component
import { ClientShell } from './ClientShell';
import { ServerHeavy } from './ServerHeavy';
export default function Page() {
return (
<ClientShell>
<ServerHeavy /> {/* still a Server Component */}
</ClientShell>
);
}
Server Component 可以 async
Server Component 支持顶层 await,这就是它存在的核心意义。直接在组件体里 fetch,不用 useEffect、不用 SWR、不用 loader。
React Context 依赖 hooks,所以 provider 组件必须是 Client Component。在靠近根的位置包一层,作为 children 渲染的 Server Component 照常工作,children 是透传过去的,不会在客户端重渲染。
'use client';
import { createContext } from 'react';
export const ThemeContext = createContext('light');
export function ThemeProvider({ children }: { children: React.ReactNode }) {
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>;
}
// app/layout.tsx: <ThemeProvider>{children}</ThemeProvider>
connection():显式退出静态渲染
await connection()(来自 next/server)告诉 Next 这次渲染必须等到真实请求到来,即便没读 cookies 或 headers 也强制动态渲染。比旧的 unstable_noStore 更干净地表达"这段必须每个请求都跑"。
import { connection } from 'next/server';
export default async function Page() {
await connection();
const rnd = Math.random(); // now safe: runs per request, not at build
return <p>{rnd}</p>;
}
import { after } from 'next/server';
export default async function Page() {
after(async () => {
await logPageView(); // runs after the user already has the HTML
});
return <Content />;
}
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'], variable: '--font-sans' });
export default function Layout({ children }: { children: React.ReactNode }) {
return <html className={inter.variable}><body>{children}</body></html>;
}
// tailwind.config: fontFamily: { sans: ['var(--font-sans)'] }
多字重与 adjustFontFallback 默认值
可变字体不用列 weight;静态字体只传你真正用到的字重,bundle 才小。next/font 还会自动算出一个尺寸对齐的系统兜底字体,在 web 字体加载时减少布局抖动。
import { Roboto } from 'next/font/google';
const roboto = Roboto({
subsets: ['latin'],
weight: ['400', '700'], // only what you use
display: 'swap',
});
# .env.local
DATABASE_URL=postgres://... # server-only
NEXT_PUBLIC_GA_ID=G-XXXXXXX # client-visible
// usage
const db = process.env.DATABASE_URL; // OK on server only
const ga = process.env.NEXT_PUBLIC_GA_ID; // OK anywhere
// instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('./otel'); // start tracing in the Node runtime only
}
}
// WRONG
'use client';
export default async function Bad() { /* renders a Promise */ }
// RIGHT — pass a promise prop and consume it with use()
'use client';
import { use } from 'react';
export default function Good({ p }: { p: Promise<X> }) {
const x = use(p);
return <pre>{JSON.stringify(x)}</pre>;
}
// from a Client Component
'use client';
import { createTodo } from '@/app/actions';
<button onClick={() => createTodo(formData)}>Add</button>
// or from a form
<form action={createTodo}><input name="title" /></form>
// WRONG — directive below import is ignored
import { z } from 'zod';
'use server'; // silently does nothing
// RIGHT
'use server';
import { z } from 'zod';
'use client';
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus(); // sees the parent <form>
return <button disabled={pending}>{pending ? 'Saving...' : 'Save'}</button>;
}
// parent:
// <form action={action}><SubmitButton /></form>
坑:middleware 不能用 Node API
middleware 跑在 Edge runtime,没有 fs、path、crypto.createHash,任何 Node 专属 API 都不能用。改用 Web Crypto、fetch、URL、Request、Response。真要用 Node 就把工作挪到 route handler。
// WRONG
import fs from 'node:fs'; // build fails
// RIGHT — Web Crypto in middleware
const hash = await crypto.subtle.digest(
'SHA-256',
new TextEncoder().encode('hello'),
);
// next build runs with NEXT_PUBLIC_API=https://prod.acme.com
// process.env.NEXT_PUBLIC_API is REPLACED at build:
console.log(process.env.NEXT_PUBLIC_API); // "https://prod.acme.com"
// changing env at runtime does NOT change the client bundle.
坑:跨服务端/客户端边界传函数
Server Component 给 Client Component 只能传可序列化的 props(字符串、数字、普通对象、JSX、Server Action)。 不能 传普通函数或类实例,不可序列化,构建会报错。
// WRONG — passing a function from Server → Client
<ClientChild onClick={() => doStuff()} /> // build error
// RIGHT — pass a Server Action (special-cased)
import { doStuff } from './actions';
<ClientChild action={doStuff} />
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function action() {
await mutate();
revalidatePath('/todos'); // mark /todos stale
redirect('/todos'); // next render serves fresh data
}
// WRONG — personalized data accidentally static
const me = await fetch('/api/me').then((r) => r.json()); // cached!
// Every visitor sees the first build user's data.
// RIGHT
const me = await fetch('/api/me', { cache: 'no-store' }).then((r) => r.json());
import { cache } from 'react';
export const getUser = cache(async (id: string) =>
db.user.findUnique({ where: { id } }),
);
// safe to call from any number of Server Components — dedupes within one request
// WRONG
try {
const u = await getUser();
if (!u) redirect('/login'); // caught and swallowed below
} catch (e) { console.error(e); }
// RIGHT — call after the try, or rethrow framework errors
const u = await getUser();
if (!u) redirect('/login');
坑:Server Action 返回值必须可序列化
Server Action 返回的东西会发回客户端,所以必须可序列化:普通对象、字符串、数字、日期、数组。返回类实例、函数或数据库游标会失败。先映射成普通结构。
// WRONG — returns a Prisma model instance with methods
export async function getTodo(id: string) { return db.todo.findUnique({ where: { id } }); }
// RIGHT — return a plain object
export async function getTodo(id: string) {
const t = await db.todo.findUnique({ where: { id } });
return t ? { id: t.id, title: t.title, done: t.done } : null;
}
// this single line forces the whole route dynamic
import { cookies } from 'next/headers';
const theme = (await cookies()).get('theme')?.value;
// if you only need it client-side, read document.cookie in a Client Component instead
// WRONG — top-level window in a shared util
export const origin = window.location.origin; // crashes on the server
// RIGHT — defer to call time, guard the environment
export const getOrigin = () =>
typeof window === 'undefined' ? process.env.SITE_URL! : window.location.origin;
坑:串行 await 制造请求瀑布
一个接一个的独立 await 是串行执行的,总耗时是加和而不是取最大。如果请求 B 不需要 A 的结果,用 Promise.all 同时发。只有真的有数据依赖时才串起来。
// WRONG — waterfall, ~2x slower
const user = await getUser(id);
const posts = await getPosts(id); // does not depend on user
// RIGHT — parallel
const [user, posts] = await Promise.all([getUser(id), getPosts(id)]);
// RIGHT — pin locale + timeZone so server and client agree
const fmt = new Intl.DateTimeFormat('en-US', {
timeZone: 'UTC',
dateStyle: 'medium',
});
return <time>{fmt.format(new Date(post.createdAt))}</time>;
坑:桶文件 import 拖大客户端 bundle
从一个巨大的 index 桶文件里 import 单个图标(import { Icon } from "lib")可能把整个包拖进客户端 bundle。直接 import 深层路径,或用 optimizePackageImports 让 Next 对桶文件做 tree-shake。
// next.config.ts
export default {
experimental: { optimizePackageImports: ['lucide-react', '@mui/icons-material'] },
};
// now: import { Camera } from 'lucide-react' only ships Camera
坑:在 Server Component 渲染里改数据
Server Component 的渲染可能跑多次(还会被缓存),所以必须无副作用。在渲染里写数据库、发邮件、发 POST 都是 bug,这些应放进由用户操作触发的 Server Action 或 route handler。
// WRONG — write during render, runs on every (cached) render
export default async function Page() {
await db.view.create({ data: { at: new Date() } }); // side effect!
return <Article />;
}
// RIGHT — log after the response with after(), or in a route handler