nextjs-15

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

When to Use

适用场景

Triggers: When building Next.js apps, working with app router, server/client components, or API routes.
Load when: building Next.js 15 apps, using app router, implementing server actions, fetching data, or setting up middleware.
触发场景:构建Next.js应用、使用App Router、开发服务端/客户端组件或API路由时。
适用时机:构建Next.js 15应用、使用App Router、实现Server Actions、获取数据或配置中间件时。

Critical Patterns

核心模式

Pattern 1: Server Components by default

模式1:默认使用Server Components

typescript
// ✅ Server Component — async by default, no directive needed
async function UserProfile({ userId }: { userId: string }) {
  const user = await db.users.findById(userId); // Direct DB access
  return <ProfileCard user={user} />;
}

// ✅ Client Component — only when you need interactivity
'use client';
function LikeButton({ postId }: { postId: string }) {
  const [liked, setLiked] = useState(false);
  return <button onClick={() => setLiked(!liked)}>{liked ? '❤️' : '🤍'}</button>;
}
typescript
// ✅ Server Component — 默认支持异步,无需指令
async function UserProfile({ userId }: { userId: string }) {
  const user = await db.users.findById(userId); // 直接访问数据库
  return <ProfileCard user={user} />;
}

// ✅ Client Component — 仅在需要交互时使用
'use client';
function LikeButton({ postId }: { postId: string }) {
  const [liked, setLiked] = useState(false);
  return <button onClick={() => setLiked(!liked)}>{liked ? '❤️' : '🤍'}</button>;
}

Pattern 2: Server Actions

模式2:Server Actions

typescript
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';

async function createUser(formData: FormData) {
  const name = formData.get('name') as string;
  const email = formData.get('email') as string;

  await db.users.create({ name, email });
  revalidatePath('/users');
  redirect('/users');
}

// Direct usage in form
export default function CreateUserPage() {
  return (
    <form action={createUser}>
      <input name="name" required />
      <input name="email" type="email" required />
      <button type="submit">Create</button>
    </form>
  );
}
typescript
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';

async function createUser(formData: FormData) {
  const name = formData.get('name') as string;
  const email = formData.get('email') as string;

  await db.users.create({ name, email });
  revalidatePath('/users');
  redirect('/users');
}

// 在表单中直接使用
export default function CreateUserPage() {
  return (
    <form action={createUser}>
      <input name="name" required />
      <input name="email" type="email" required />
      <button type="submit">Create</button>
    </form>
  );
}

Pattern 3: Prevent client-side access with server-only

模式3:使用server-only阻止客户端访问

typescript
// lib/db.ts
import 'server-only'; // Build error if imported in client

export async function getSecretData() {
  return db.secrets.findAll();
}
typescript
// lib/db.ts
import 'server-only'; // 若在客户端导入会触发构建错误

export async function getSecretData() {
  return db.secrets.findAll();
}

Code Examples

代码示例

Data Fetching — Parallel and Streaming

数据获取 — 并行与流式传输

typescript
// ✅ Parallel fetching in Server Component
async function Dashboard() {
  const [user, posts, stats] = await Promise.all([
    getUser(),
    getPosts(),
    getStats(),
  ]);
  return <DashboardView user={user} posts={posts} stats={stats} />;
}

// ✅ Streaming with Suspense
import { Suspense } from 'react';

export default function Page() {
  return (
    <div>
      <Header /> {/* Immediate */}
      <Suspense fallback={<PostsSkeleton />}>
        <Posts /> {/* Streams when ready */}
      </Suspense>
    </div>
  );
}

async function Posts() {
  const posts = await getPosts(); // Waits here
  return <PostList posts={posts} />;
}
typescript
// ✅ Server Component中的并行获取
async function Dashboard() {
  const [user, posts, stats] = await Promise.all([
    getUser(),
    getPosts(),
    getStats(),
  ]);
  return <DashboardView user={user} posts={posts} stats={stats} />;
}

// ✅ 结合Suspense实现流式传输
import { Suspense } from 'react';

export default function Page() {
  return (
    <div>
      <Header /> {/* 立即渲染 */}
      <Suspense fallback={<PostsSkeleton />}>
        <Posts /> {/* 准备就绪后流式渲染 */}
      </Suspense>
    </div>
  );
}

async function Posts() {
  const posts = await getPosts(); // 在此处等待数据
  return <PostList posts={posts} />;
}

API Route Handler

API路由处理器

typescript
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const page = searchParams.get('page') ?? '1';

  const users = await db.users.findMany({ page: parseInt(page) });
  return NextResponse.json(users);
}

export async function POST(request: NextRequest) {
  const body = await request.json();
  const user = await db.users.create(body);
  return NextResponse.json(user, { status: 201 });
}
typescript
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const page = searchParams.get('page') ?? '1';

  const users = await db.users.findMany({ page: parseInt(page) });
  return NextResponse.json(users);
}

export async function POST(request: NextRequest) {
  const body = await request.json();
  const user = await db.users.create(body);
  return NextResponse.json(user, { status: 201 });
}

Middleware — Route Protection

中间件 — 路由保护

typescript
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest) {
  const token = request.cookies.get('token')?.value;
  const isProtected = request.nextUrl.pathname.startsWith('/dashboard');

  if (isProtected && !token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*', '/admin/:path*'],
};
typescript
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest) {
  const token = request.cookies.get('token')?.value;
  const isProtected = request.nextUrl.pathname.startsWith('/dashboard');

  if (isProtected && !token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*', '/admin/:path*'],
};

Metadata — Static and Dynamic

元数据 — 静态与动态

typescript
// Static
export const metadata = {
  title: 'My App',
  description: 'App description',
};

// Dynamic
export async function generateMetadata({ params }: { params: { id: string } }) {
  const post = await getPost(params.id);
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: { images: [post.coverImage] },
  };
}
typescript
// 静态元数据
export const metadata = {
  title: 'My App',
  description: 'App description',
};

// 动态元数据
export async function generateMetadata({ params }: { params: { id: string } }) {
  const post = await getPost(params.id);
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: { images: [post.coverImage] },
  };
}

Route Groups and Layouts

路由组与布局

app/
├── (auth)/              # Group with no URL impact
│   ├── layout.tsx       # Layout only for auth pages
│   ├── login/page.tsx   # /login
│   └── register/page.tsx # /register
├── (dashboard)/
│   ├── layout.tsx       # Dashboard layout
│   └── overview/page.tsx # /overview
├── _components/         # Private folder (not a route)
├── layout.tsx           # Root layout (required)
└── page.tsx             # /
app/
├── (auth)/              # 无URL影响的路由组
│   ├── layout.tsx       # 仅适用于认证页面的布局
│   ├── login/page.tsx   # 对应路由 /login
│   └── register/page.tsx # 对应路由 /register
├── (dashboard)/
│   ├── layout.tsx       # 仪表盘布局
│   └── overview/page.tsx # 对应路由 /overview
├── _components/         # 私有文件夹(非路由)
├── layout.tsx           # 根布局(必填)
└── page.tsx             # 对应路由 /

Anti-Patterns

反模式

❌ Fetch in Client Component when it could be Server

❌ 可在服务端完成时仍在Client Component中获取数据

typescript
// ❌ Unnecessary
'use client';
function UserList() {
  const [users, setUsers] = useState([]);
  useEffect(() => {
    fetch('/api/users').then(r => r.json()).then(setUsers);
  }, []);
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

// ✅ Direct Server Component
async function UserList() {
  const users = await db.users.findMany();
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
typescript
// ❌ 不必要的写法
'use client';
function UserList() {
  const [users, setUsers] = useState([]);
  useEffect(() => {
    fetch('/api/users').then(r => r.json()).then(setUsers);
  }, []);
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

// ✅ 直接使用Server Component
async function UserList() {
  const users = await db.users.findMany();
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

❌ 'use client' in layout or page

❌ 在布局或页面中添加'use client'

typescript
// ❌ Makes the entire tree client-side
'use client';
export default function Layout({ children }) { /* ... */ }

// ✅ Isolate the client component
export default function Layout({ children }) {
  return <div><NavBar />{children}</div>; // NavBar can be 'use client'
}
typescript
// ❌ 会导致整个组件树转为客户端渲染
'use client';
export default function Layout({ children }) { /* ... */ }

// ✅ 隔离客户端组件
export default function Layout({ children }) {
  return <div><NavBar />{children}</div>; // NavBar可以标记为'use client'
}

Quick Reference

速查手册

TaskPattern
DB in componentServer Component + async/await
Form
<form action={serverAction}>
Invalidate cache
revalidatePath('/path')
Redirect
redirect('/path')
(server-only import)
URL params
{ params }: { params: { id: string } }
Search params
searchParams.get('key')
in Server Component
Protect routes
middleware.ts
at root
Prevent client bundle
import 'server-only'
任务实现模式
在组件中访问数据库Server Component + async/await
表单实现
<form action={serverAction}>
失效缓存
revalidatePath('/path')
页面重定向
redirect('/path')
(仅服务端导入)
URL参数
{ params }: { params: { id: string } }
查询参数在Server Component中使用
searchParams.get('key')
路由保护在根目录创建
middleware.ts
阻止客户端打包
import 'server-only'

Rules

规则

  • Server Components are the default; add
    'use client'
    only when the component requires browser APIs, event handlers, or React state
  • Never add
    'use client'
    to layout or page files — this forces the entire subtree client-side and defeats Server Component benefits
  • Server Actions (
    'use server'
    ) must be the mechanism for mutations from forms; avoid client-side fetch for form submissions
  • revalidatePath
    or
    revalidateTag
    must be called after mutations that change cached data; stale caches are a correctness bug
  • import 'server-only'
    must be added to any module that accesses secrets, databases, or server-only APIs to prevent accidental client bundling
  • Server Components为默认选项;仅当组件需要浏览器API、事件处理器或React状态时,才添加
    'use client'
    指令
  • 切勿在布局或页面文件中添加
    'use client'
    ——这会强制整个子树转为客户端渲染,丧失Server Component的优势
  • 表单提交的变更操作必须通过Server Actions(
    'use server'
    )实现;避免使用客户端fetch提交表单
  • 在变更缓存数据后,必须调用
    revalidatePath
    revalidateTag
    ;缓存过期会导致正确性问题
  • 任何访问密钥、数据库或仅服务端API的模块,必须添加
    import 'server-only'
    ,防止意外被打包到客户端