app-router-helper

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

App 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
app/
directory with Server Components by default.
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+)默认在
app/
目录中使用文件系统路由,并且默认采用Server Components。
核心概念:
  • 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:
  • layout.tsx
    : Shared UI, doesn't re-render
  • page.tsx
    : Unique UI for route
  • loading.tsx
    : Loading state (Suspense boundary)
  • error.tsx
    : Error boundary
  • template.tsx
    : Re-renders on navigation
  • route.ts
    : API endpoint
基础结构:
app/
├── layout.tsx          # 根布局(必填)
├── page.tsx            # 首页(/)
├── loading.tsx         # 加载状态UI
├── error.tsx           # 错误状态UI
├── not-found.tsx       # 404页面
└── about/
    └── page.tsx        # 关于页面(/about)
特殊文件:
  • layout.tsx
    :共享UI,导航时不会重新渲染
  • page.tsx
    :对应路由的唯一UI
  • loading.tsx
    :加载状态(Suspense边界)
  • error.tsx
    :错误边界
  • template.tsx
    :导航时会重新渲染
  • route.ts
    :API端点

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    # /products
Dynamic routes:
app/
└── products/
    └── [id]/
        └── page.tsx    # /products/123
typescript
// 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/123
typescript
// 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/c

Step 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.tsx
typescript
// 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.tsx
typescript
// 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

最佳实践

  1. Default to Server Components: Only use 'use client' when needed
  2. Fetch data where needed: Co-locate data fetching with components
  3. Use layouts: Share UI and avoid re-renders
  4. Implement loading states: Use loading.tsx and Suspense
  5. Handle errors: Add error.tsx boundaries
  6. Optimize metadata: Use generateMetadata for SEO
  7. Stream content: Use Suspense for better UX
  8. Type everything: Use TypeScript for params and props
  1. 默认使用Server Components:仅在必要时使用'use client'
  2. 就近获取数据:将数据获取与组件放在一起
  3. 使用布局:共享UI并避免重复渲染
  4. 实现加载状态:使用loading.tsx和Suspense
  5. 处理错误:添加error.tsx边界
  6. 优化元数据:使用generateMetadata提升SEO
  7. 流式传输内容:使用Suspense提升用户体验
  8. 全面类型化:为参数和props使用TypeScript