nextjs-app-router
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseNext.js App Router
Next.js App Router
Server vs Client Components
Server Components 与 Client Components
Default to Server Components. Only add when you need:
'use client'- Event handlers (onClick, onChange, onSubmit)
- Browser APIs (localStorage, window, navigator)
- React hooks (useState, useEffect, useRef)
- Third-party client libraries
tsx
// Server Component (default) - no directive needed
export default async function Page() {
const data = await fetchData(); // Direct async/await
return <div>{data.title}</div>;
}
// Client Component - explicit directive
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}默认使用Server Components。 仅在需要以下功能时添加指令:
'use client'- 事件处理程序(onClick、onChange、onSubmit)
- 浏览器API(localStorage、window、navigator)
- React钩子(useState、useEffect、useRef)
- 第三方客户端库
tsx
// Server Component (default) - no directive needed
export default async function Page() {
const data = await fetchData(); // Direct async/await
return <div>{data.title}</div>;
}
// Client Component - explicit directive
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}Next.js 15 Async Params (Critical)
Next.js 15 异步参数(重点)
Params and searchParams are now Promises and must be awaited:
tsx
// ✅ Correct - Next.js 15
type Props = {
params: Promise<{ locale: string; slug: string }>;
searchParams: Promise<{ [key: string]: string | undefined }>;
};
export default async function Page({ params, searchParams }: Props) {
const { locale, slug } = await params;
const { theme } = await searchParams;
return <div>Locale: {locale}, Slug: {slug}</div>;
}
// ✅ generateMetadata also uses async params
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale } = await params;
return { title: `Page - ${locale}` };
}Params和searchParams现在是Promise对象,必须进行 await 处理:
tsx
// ✅ Correct - Next.js 15
type Props = {
params: Promise<{ locale: string; slug: string }>;
searchParams: Promise<{ [key: string]: string | undefined }>;
};
export default async function Page({ params, searchParams }: Props) {
const { locale, slug } = await params;
const { theme } = await searchParams;
return <div>Locale: {locale}, Slug: {slug}</div>;
}
// ✅ generateMetadata also uses async params
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale } = await params;
return { title: `Page - ${locale}` };
}Route File Conventions
路由文件约定
app/
├── layout.tsx # Root layout (required)
├── page.tsx # Home page (/)
├── loading.tsx # Loading UI (Suspense boundary)
├── error.tsx # Error boundary ('use client' required)
├── not-found.tsx # 404 page
├── [locale]/
│ ├── layout.tsx # Nested layout
│ ├── page.tsx # /[locale]
│ └── services/
│ ├── page.tsx # /[locale]/services
│ └── [slug]/
│ └── page.tsx # /[locale]/services/[slug]
└── api/
└── route.ts # API route handlerapp/
├── layout.tsx # 根布局(必填)
├── page.tsx # 首页 (/)
├── loading.tsx # 加载UI(Suspense 边界)
├── error.tsx # 错误边界(需添加'use client')
├── not-found.tsx # 404页面
├── [locale]/
│ ├── layout.tsx # 嵌套布局
│ ├── page.tsx # /[locale]
│ └── services/
│ ├── page.tsx # /[locale]/services
│ └── [slug]/
│ └── page.tsx # /[locale]/services/[slug]
└── api/
└── route.ts # API路由处理器Layouts and Templates
布局与模板
tsx
// app/[locale]/layout.tsx
export default async function LocaleLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
return (
<html lang={locale}>
<body>{children}</body>
</html>
);
}tsx
// app/[locale]/layout.tsx
export default async function LocaleLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
return (
<html lang={locale}>
<body>{children}</body>
</html>
);
}Server Actions
Server Actions
tsx
// lib/actions.ts
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
import { redirect } from 'next/navigation';
export async function submitForm(formData: FormData) {
const email = formData.get('email') as string;
// Validate with Zod (see zod-react-hook-form skill)
// Process data...
revalidatePath('/[locale]/contact'); // Revalidate specific path
// OR revalidateTag('contact-submissions'); // Revalidate by tag
redirect('/success'); // Optional redirect
}
// Usage in Client Component
'use client';
export function ContactForm() {
return (
<form action={submitForm}>
<input name="email" type="email" required />
<button type="submit">Submit</button>
</form>
);
}tsx
// lib/actions.ts
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
import { redirect } from 'next/navigation';
export async function submitForm(formData: FormData) {
const email = formData.get('email') as string;
// Validate with Zod (see zod-react-hook-form skill)
// Process data...
revalidatePath('/[locale]/contact'); // Revalidate specific path
// OR revalidateTag('contact-submissions'); // Revalidate by tag
redirect('/success'); // Optional redirect
}
// Usage in Client Component
'use client';
export function ContactForm() {
return (
<form action={submitForm}>
<input name="email" type="email" required />
<button type="submit">Submit</button>
</form>
);
}Route Handlers (API Routes)
路由处理器(API路由)
tsx
// app/api/webhook/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
const body = await request.json();
// Process webhook...
return NextResponse.json({ success: true }, { status: 200 });
}
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const id = searchParams.get('id');
return NextResponse.json({ id });
}tsx
// app/api/webhook/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
const body = await request.json();
// Process webhook...
return NextResponse.json({ success: true }, { status: 200 });
}
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const id = searchParams.get('id');
return NextResponse.json({ id });
}Data Fetching Patterns
数据获取模式
tsx
// Server Component with fetch
async function getData() {
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 }, // ISR: revalidate every hour
// OR cache: 'no-store', // SSR: always fresh
// OR next: { tags: ['data'] }, // On-demand with revalidateTag
});
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
}
export default async function Page() {
const data = await getData();
return <div>{data.title}</div>;
}tsx
// Server Component with fetch
async function getData() {
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 }, // ISR: 每小时重新验证
// OR cache: 'no-store', // SSR: 始终获取最新数据
// OR next: { tags: ['data'] }, // 按需重新验证(使用revalidateTag)
});
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
}
export default async function Page() {
const data = await getData();
return <div>{data.title}</div>;
}Static Generation
静态生成
tsx
// Generate static params for dynamic routes
export async function generateStaticParams() {
const locales = ['pt-PT', 'en', 'tr', 'es', 'fr', 'de'];
const services = await getServices();
return locales.flatMap(locale =>
services.map(service => ({
locale,
slug: service.slug,
}))
);
}tsx
// Generate static params for dynamic routes
export async function generateStaticParams() {
const locales = ['pt-PT', 'en', 'tr', 'es', 'fr', 'de'];
const services = await getServices();
return locales.flatMap(locale =>
services.map(service => ({
locale,
slug: service.slug,
}))
);
}Anti-Patterns to Avoid
需避免的反模式
tsx
// ❌ Don't use params directly without awaiting (Next.js 15)
export default function Page({ params }: { params: { id: string } }) {
return <div>{params.id}</div>; // Will cause errors
}
// ❌ Don't fetch in Client Components when Server Components work
'use client';
export default function Page() {
const [data, setData] = useState(null);
useEffect(() => { fetch('/api/data')... }, []); // Unnecessary
}
// ❌ Don't use 'use client' on entire pages unless necessary
'use client';
export default function Page() {
return <div>Static content</div>; // Should be Server Component
}
// ❌ Don't import Server Components into Client Components
// Server Components can only be passed as children/propstsx
// ❌ Don't use params directly without awaiting (Next.js 15)
export default function Page({ params }: { params: { id: string } }) {
return <div>{params.id}</div>; // Will cause errors
}
// ❌ Don't fetch in Client Components when Server Components work
'use client';
export default function Page() {
const [data, setData] = useState(null);
useEffect(() => { fetch('/api/data')... }, []); // Unnecessary
}
// ❌ Don't use 'use client' on entire pages unless necessary
'use client';
export default function Page() {
return <div>Static content</div>; // Should be Server Component
}
// ❌ Don't import Server Components into Client Components
// Server Components can only be passed as children/propsReferences
参考资料
For detailed patterns, see:
- PATTERNS.md - Advanced composition patterns
- DATA-FETCHING.md - Caching strategies
如需了解详细模式,请查看:
- PATTERNS.md - 高级组合模式
- DATA-FETCHING.md - 缓存策略