app-router-helper
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseApp Router Helper
App Router 实用指南
Implement Next.js App Router patterns for modern React applications.
为现代React应用实现Next.js App Router模式。
Quick Start
快速入门
App Router (Next.js 13+) uses file-system routing in the directory with Server Components by default.
app/Key concepts:
- Server Components (default): Render on server, reduce bundle size
- Client Components ('use client'): Interactive, use hooks
- Layouts: Shared UI across routes
- Loading/Error: Automatic UI states
App Router(Next.js 13+)默认在目录中使用文件系统路由,并且默认采用Server Components。
app/核心概念:
- Server Components(默认):在服务器端渲染,减小包体积
- Client Components(需声明'use client'):支持交互,可使用hooks
- Layouts:跨路由共享UI
- Loading/Error:自动处理UI状态
Instructions
操作步骤
Step 1: Understand File Structure
步骤1:了解文件结构
Basic structure:
app/
├── layout.tsx # Root layout (required)
├── page.tsx # Home page (/)
├── loading.tsx # Loading UI
├── error.tsx # Error UI
├── not-found.tsx # 404 page
└── about/
└── page.tsx # About page (/about)Special files:
- : Shared UI, doesn't re-render
layout.tsx - : Unique UI for route
page.tsx - : Loading state (Suspense boundary)
loading.tsx - : Error boundary
error.tsx - : Re-renders on navigation
template.tsx - : API endpoint
route.ts
基础结构:
app/
├── layout.tsx # 根布局(必填)
├── page.tsx # 首页(/)
├── loading.tsx # 加载状态UI
├── error.tsx # 错误状态UI
├── not-found.tsx # 404页面
└── about/
└── page.tsx # 关于页面(/about)特殊文件:
- :共享UI,导航时不会重新渲染
layout.tsx - :对应路由的唯一UI
page.tsx - :加载状态(Suspense边界)
loading.tsx - :错误边界
error.tsx - :导航时会重新渲染
template.tsx - :API端点
route.ts
Step 2: Create Layouts
步骤2:创建布局
Root layout (required):
typescript
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<Header />
{children}
<Footer />
</body>
</html>
);
}Nested layouts:
typescript
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div>
<Sidebar />
<main>{children}</main>
</div>
);
}Layouts persist across navigation and don't re-render.
根布局(必填):
typescript
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<Header />
{children}
<Footer />
</body>
</html>
);
}嵌套布局:
typescript
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div>
<Sidebar />
<main>{children}</main>
</div>
);
}布局在导航过程中会保持持久化,不会重新渲染。
Step 3: Server vs Client Components
步骤3:Server Components vs Client Components
Server Component (default):
typescript
// app/products/page.tsx
// No 'use client' = Server Component
async function ProductsPage() {
// Can fetch data directly
const products = await db.products.findMany();
return (
<div>
{products.map(p => (
<ProductCard key={p.id} product={p} />
))}
</div>
);
}
export default ProductsPage;Client Component:
typescript
// app/components/AddToCart.tsx
'use client';
import { useState } from 'react';
export function AddToCart({ productId }: { productId: string }) {
const [count, setCount] = useState(1);
const handleAdd = () => {
// Client-side logic
addToCart(productId, count);
};
return (
<div>
<button onClick={() => setCount(count - 1)}>-</button>
<span>{count}</span>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={handleAdd}>Add to Cart</button>
</div>
);
}When to use 'use client':
- Event handlers (onClick, onChange)
- React hooks (useState, useEffect, useContext)
- Browser APIs (localStorage, window)
- Third-party libraries requiring client
Server Components(默认):
typescript
// app/products/page.tsx
// 无需'use client' = Server Components
async function ProductsPage() {
// 可直接在组件中获取数据
const products = await db.products.findMany();
return (
<div>
{products.map(p => (
<ProductCard key={p.id} product={p} />
))}
</div>
);
}
export default ProductsPage;Client Components:
typescript
// app/components/AddToCart.tsx
'use client';
import { useState } from 'react';
export function AddToCart({ productId }: { productId: string }) {
const [count, setCount] = useState(1);
const handleAdd = () => {
// 客户端逻辑
addToCart(productId, count);
};
return (
<div>
<button onClick={() => setCount(count - 1)}>-</button>
<span>{count}</span>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={handleAdd}>Add to Cart</button>
</div>
);
}何时使用'use client':
- 需要事件处理函数(onClick、onChange等)
- 使用React hooks(useState、useEffect、useContext等)
- 调用浏览器API(localStorage、window等)
- 使用依赖客户端环境的第三方库
Step 4: Implement Data Fetching
步骤4:实现数据获取
Server Component data fetching:
typescript
// app/posts/page.tsx
async function PostsPage() {
// Fetch in Server Component
const posts = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 } // Cache for 1 hour
}).then(res => res.json());
return <PostList posts={posts} />;
}Parallel data fetching:
typescript
async function Page() {
// Fetch in parallel
const [user, posts] = await Promise.all([
fetchUser(),
fetchPosts(),
]);
return (
<div>
<UserProfile user={user} />
<PostList posts={posts} />
</div>
);
}Sequential data fetching:
typescript
async function Page() {
const user = await fetchUser();
const posts = await fetchUserPosts(user.id); // Depends on user
return <div>...</div>;
}Server Components中的数据获取:
typescript
// app/posts/page.tsx
async function PostsPage() {
// 在Server Components中获取数据
const posts = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 } // 缓存1小时
}).then(res => res.json());
return <PostList posts={posts} />;
}并行数据获取:
typescript
async function Page() {
// 并行获取数据
const [user, posts] = await Promise.all([
fetchUser(),
fetchPosts(),
]);
return (
<div>
<UserProfile user={user} />
<PostList posts={posts} />
</div>
);
}串行数据获取:
typescript
async function Page() {
const user = await fetchUser();
const posts = await fetchUserPosts(user.id); // 依赖用户数据
return <div>...</div>;
}Step 5: Organize Routes
步骤5:组织路由
Route groups (don't affect URL):
app/
├── (marketing)/
│ ├── layout.tsx # Marketing layout
│ ├── about/
│ │ └── page.tsx # /about
│ └── contact/
│ └── page.tsx # /contact
└── (shop)/
├── layout.tsx # Shop layout
└── products/
└── page.tsx # /productsDynamic routes:
app/
└── products/
└── [id]/
└── page.tsx # /products/123typescript
// app/products/[id]/page.tsx
export default function ProductPage({
params,
}: {
params: { id: string }
}) {
return <div>Product {params.id}</div>;
}Catch-all routes:
app/
└── docs/
└── [...slug]/
└── page.tsx # /docs/a, /docs/a/b, /docs/a/b/c路由组(不影响URL):
app/
├── (marketing)/
│ ├── layout.tsx # 营销站点布局
│ ├── about/
│ │ └── page.tsx # /about
│ └── contact/
│ └── page.tsx # /contact
└── (shop)/
├── layout.tsx # 商城布局
└── products/
└── page.tsx # /products动态路由:
app/
└── products/
└── [id]/
└── page.tsx # /products/123typescript
// app/products/[id]/page.tsx
export default function ProductPage({
params,
}: {
params: { id: string }
}) {
return <div>Product {params.id}</div>;
}兜底路由:
app/
└── docs/
└── [...slug]/
└── page.tsx # /docs/a, /docs/a/b, /docs/a/b/cStep 6: Handle Loading and Errors
步骤6:处理加载和错误状态
Loading UI:
typescript
// app/dashboard/loading.tsx
export default function Loading() {
return <div>Loading dashboard...</div>;
}Error handling:
typescript
// app/dashboard/error.tsx
'use client'; // Error components must be Client Components
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
);
}Not found:
typescript
// app/not-found.tsx
export default function NotFound() {
return <div>404 - Page Not Found</div>;
}加载状态UI:
typescript
// app/dashboard/loading.tsx
export default function Loading() {
return <div>加载控制台中...</div>;
}错误处理:
typescript
// app/dashboard/error.tsx
'use client'; // 错误组件必须是Client Components
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>出现错误!</h2>
<button onClick={reset}>重试</button>
</div>
);
}404页面:
typescript
// app/not-found.tsx
export default function NotFound() {
return <div>404 - 页面未找到</div>;
}Common Patterns
常见模式
Streaming with Suspense
使用Suspense实现流式渲染
typescript
import { Suspense } from 'react';
export default function Page() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<Skeleton />}>
<SlowComponent />
</Suspense>
<FastComponent />
</div>
);
}typescript
import { Suspense } from 'react';
export default function Page() {
return (
<div>
<h1>控制台</h1>
<Suspense fallback={<Skeleton />}>
<SlowComponent />
</Suspense>
<FastComponent />
</div>
);
}Parallel Routes
并行路由
app/
└── dashboard/
├── layout.tsx
├── @analytics/
│ └── page.tsx
├── @team/
│ └── page.tsx
└── page.tsxtypescript
// app/dashboard/layout.tsx
export default function Layout({
children,
analytics,
team,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
}) {
return (
<div>
{children}
<div className="grid">
{analytics}
{team}
</div>
</div>
);
}app/
└── dashboard/
├── layout.tsx
├── @analytics/
│ └── page.tsx
├── @team/
│ └── page.tsx
└── page.tsxtypescript
// app/dashboard/layout.tsx
export default function Layout({
children,
analytics,
team,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
}) {
return (
<div>
{children}
<div className="grid">
{analytics}
{team}
</div>
</div>
);
}Intercepting Routes
拦截路由
app/
└── photos/
├── [id]/
│ └── page.tsx
└── (.)[id]/
└── page.tsx # Intercepts /photos/[id]app/
└── photos/
├── [id]/
│ └── page.tsx
└── (.)[id]/
└── page.tsx # 拦截/photos/[id]Metadata
元数据配置
typescript
// app/products/[id]/page.tsx
import { Metadata } from 'next';
export async function generateMetadata({
params,
}: {
params: { id: string }
}): Promise<Metadata> {
const product = await fetchProduct(params.id);
return {
title: product.name,
description: product.description,
openGraph: {
images: [product.image],
},
};
}typescript
// app/products/[id]/page.tsx
import { Metadata } from 'next';
export async function generateMetadata({
params,
}: {
params: { id: string }
}): Promise<Metadata> {
const product = await fetchProduct(params.id);
return {
title: product.name,
description: product.description,
openGraph: {
images: [product.image],
},
};
}API Routes
API路由
typescript
// app/api/products/route.ts
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
const products = await db.products.findMany();
return NextResponse.json(products);
}
export async function POST(request: Request) {
const body = await request.json();
const product = await db.products.create({ data: body });
return NextResponse.json(product, { status: 201 });
}Dynamic API routes:
typescript
// app/api/products/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const product = await db.products.findUnique({
where: { id: params.id }
});
return NextResponse.json(product);
}typescript
// app/api/products/route.ts
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
const products = await db.products.findMany();
return NextResponse.json(products);
}
export async function POST(request: Request) {
const body = await request.json();
const product = await db.products.create({ data: body });
return NextResponse.json(product, { status: 201 });
}动态API路由:
typescript
// app/api/products/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const product = await db.products.findUnique({
where: { id: params.id }
});
return NextResponse.json(product);
}Advanced
进阶内容
For detailed patterns:
- Server Components - Deep dive into Server Components
- Client Components - Client Component patterns
- Data Fetching - Advanced data fetching strategies
如需了解详细模式:
- Server Components - Server Components深度解析
- Client Components - Client Components模式
- Data Fetching - 进阶数据获取策略
Troubleshooting
故障排除
"use client" not working:
- Must be at top of file
- Check for Server Component imports
- Verify no async in Client Components
Data not updating:
- Check cache configuration
- Use revalidatePath or revalidateTag
- Verify fetch cache settings
Layout not applying:
- Ensure layout.tsx exists
- Check file naming (must be exact)
- Verify export default
Hydration errors:
- Server and client HTML must match
- Avoid using browser APIs in Server Components
- Check for dynamic content (dates, random)
“use client”不生效:
- 必须放在文件最顶部
- 检查是否引入了Server Components
- 确保Client Components中没有异步代码
数据未更新:
- 检查缓存配置
- 使用revalidatePath或revalidateTag
- 验证fetch缓存设置
布局未生效:
- 确保layout.tsx存在
- 检查文件名是否完全匹配
- 验证是否使用export default导出
** hydration错误:**
- 服务器端和客户端HTML必须一致
- 避免在Server Components中使用浏览器API
- 检查动态内容(日期、随机数等)
Best Practices
最佳实践
- Default to Server Components: Only use 'use client' when needed
- Fetch data where needed: Co-locate data fetching with components
- Use layouts: Share UI and avoid re-renders
- Implement loading states: Use loading.tsx and Suspense
- Handle errors: Add error.tsx boundaries
- Optimize metadata: Use generateMetadata for SEO
- Stream content: Use Suspense for better UX
- Type everything: Use TypeScript for params and props
- 默认使用Server Components:仅在必要时使用'use client'
- 就近获取数据:将数据获取与组件放在一起
- 使用布局:共享UI并避免重复渲染
- 实现加载状态:使用loading.tsx和Suspense
- 处理错误:添加error.tsx边界
- 优化元数据:使用generateMetadata提升SEO
- 流式传输内容:使用Suspense提升用户体验
- 全面类型化:为参数和props使用TypeScript