AgentSkillsCN

modern-nextjs

Next.js App Router 的最佳实践,涵盖服务器端与客户端组件、数据获取以及路由模式。适用于构建 Next.js 应用程序、设置路由、实现服务器端操作,或当用户询问“App Router”、“服务器端组件”、“Next.js 路由”或“SSR”时使用。

SKILL.md
--- frontmatter
name: modern-nextjs
description: Next.js App Router best practices including server/client components, data fetching, and routing patterns. Use when building Next.js applications, setting up routes, implementing server actions, or when user asks about "App Router", "server components", "Next.js routing", or "SSR".
category: frontend
tags: [nextjs, react, app-router, server-components, ssr]

Skill: Modern Next.js

Build performant Next.js applications with the App Router.

App Router Structure

Rules

  • ✅ DO: Use the App Router (app/ directory)
  • ✅ DO: Organize by feature/route
  • ✅ DO: Use file conventions (page.tsx, layout.tsx, loading.tsx)
  • ❌ DON'T: Mix pages and app router

File Conventions

FilePurpose
page.tsxRoute UI
layout.tsxShared layout (wraps children)
loading.tsxLoading UI (Suspense)
error.tsxError boundary
not-found.tsx404 UI
route.tsxAPI endpoint

Example Structure

code
app/
├── layout.tsx           # Root layout
├── page.tsx             # Home page (/)
├── loading.tsx          # Global loading
├── error.tsx            # Global error
├── (auth)/              # Route group (no URL segment)
│   ├── login/
│   │   └── page.tsx     # /login
│   └── register/
│       └── page.tsx     # /register
├── dashboard/
│   ├── layout.tsx       # Dashboard layout
│   ├── page.tsx         # /dashboard
│   └── settings/
│       └── page.tsx     # /dashboard/settings
└── api/
    └── users/
        └── route.tsx    # /api/users

Server vs Client Components

Rules

  • ✅ DO: Default to Server Components
  • ✅ DO: Add 'use client' only when needed
  • ✅ DO: Keep Client Components at the leaves
  • ❌ DON'T: Use 'use client' on everything
  • ❌ DON'T: Import Server Components into Client Components

When to Use Client Components

NeedComponent Type
useState, useEffectClient
Event listeners (onClick)Client
Browser APIsClient
Data fetching (await)Server
Database accessServer
Sensitive data/secretsServer

Examples

typescript
// Server Component (default) - no directive needed
async function UserList() {
  const users = await db.users.findMany(); // Direct DB access!

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

// Client Component - needs interactivity
'use client';

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(c => c + 1)}>
      Count: {count}
    </button>
  );
}

// ✅ Good - Client Component at leaf
// ServerParent.tsx (Server Component)
async function ServerParent() {
  const data = await fetchData();

  return (
    <div>
      <h1>{data.title}</h1>
      <InteractiveChild items={data.items} />
    </div>
  );
}

// InteractiveChild.tsx
'use client';
function InteractiveChild({ items }: { items: Item[] }) {
  const [selected, setSelected] = useState<string | null>(null);
  // ...
}

Data Fetching

Rules

  • ✅ DO: Fetch data in Server Components
  • ✅ DO: Use async/await directly
  • ✅ DO: Colocate data fetching with components that need it
  • ✅ DO: Use fetch with caching options
  • ❌ DON'T: Use useEffect for initial data in Server Components

Examples

typescript
// ✅ Good - fetch in Server Component
async function ProductPage({ params }: { params: { id: string } }) {
  const product = await fetch(`/api/products/${params.id}`, {
    next: { revalidate: 3600 }, // Cache for 1 hour
  }).then(res => res.json());

  return <ProductDetails product={product} />;
}

// ✅ Good - parallel data fetching
async function Dashboard() {
  // Fetch in parallel
  const [user, stats, notifications] = await Promise.all([
    fetchUser(),
    fetchStats(),
    fetchNotifications(),
  ]);

  return (
    <>
      <UserHeader user={user} />
      <StatsCards stats={stats} />
      <NotificationList notifications={notifications} />
    </>
  );
}

// ✅ Good - streaming with Suspense
async function Page() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<StatsSkeleton />}>
        <Stats /> {/* Slow component streams in */}
      </Suspense>
      <Suspense fallback={<ChartSkeleton />}>
        <Chart /> {/* Another slow component */}
      </Suspense>
    </div>
  );
}

Server Actions

Rules

  • ✅ DO: Use Server Actions for mutations
  • ✅ DO: Add 'use server' directive
  • ✅ DO: Validate input on the server
  • ✅ DO: Use revalidatePath / revalidateTag after mutations
  • ❌ DON'T: Trust client input without validation

Examples

typescript
// app/actions.ts
'use server';

import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { z } from 'zod';

const CreatePostSchema = z.object({
  title: z.string().min(1).max(100),
  content: z.string().min(1),
});

export async function createPost(formData: FormData) {
  // Validate input
  const result = CreatePostSchema.safeParse({
    title: formData.get('title'),
    content: formData.get('content'),
  });

  if (!result.success) {
    return { error: result.error.flatten() };
  }

  // Get authenticated user
  const session = await getSession();
  if (!session) {
    redirect('/login');
  }

  // Create post
  await db.posts.create({
    data: {
      ...result.data,
      authorId: session.user.id,
    },
  });

  // Revalidate and redirect
  revalidatePath('/posts');
  redirect('/posts');
}

// Usage in component
function NewPostForm() {
  return (
    <form action={createPost}>
      <input name="title" required />
      <textarea name="content" required />
      <button type="submit">Create Post</button>
    </form>
  );
}

Caching

Caching Layers

LayerDefaultOverride
Request MemoizationAutomatic for fetchN/A
Data CacheCached{ cache: 'no-store' }
Full Route CacheStatic routes cachedexport const dynamic = 'force-dynamic'

Examples

typescript
// Cached (default)
const data = await fetch(url);

// No cache
const data = await fetch(url, { cache: "no-store" });

// Revalidate after 1 hour
const data = await fetch(url, { next: { revalidate: 3600 } });

// Cache tags for on-demand revalidation
const data = await fetch(url, { next: { tags: ["posts"] } });

// Revalidate by tag
import { revalidateTag } from "next/cache";
revalidateTag("posts");

// Force dynamic rendering
export const dynamic = "force-dynamic";

Route Handlers (API Routes)

Rules

  • ✅ DO: Use for webhooks, external API integration
  • ✅ DO: Return proper status codes
  • ✅ DO: Validate request bodies
  • ❌ DON'T: Use for data accessed by your own app (use Server Components)

Examples

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

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const page = parseInt(searchParams.get("page") || "1");

  const users = await db.users.findMany({
    skip: (page - 1) * 20,
    take: 20,
  });

  return NextResponse.json(users);
}

export async function POST(request: NextRequest) {
  const body = await request.json();

  // Validate
  const result = UserSchema.safeParse(body);
  if (!result.success) {
    return NextResponse.json(
      { error: result.error.flatten() },
      { status: 400 },
    );
  }

  const user = await db.users.create({ data: result.data });

  return NextResponse.json(user, { status: 201 });
}

// Dynamic route: app/api/users/[id]/route.ts
export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } },
) {
  const user = await db.users.findUnique({ where: { id: params.id } });

  if (!user) {
    return NextResponse.json({ error: "User not found" }, { status: 404 });
  }

  return NextResponse.json(user);
}

Metadata

Rules

  • ✅ DO: Use generateMetadata for dynamic metadata
  • ✅ DO: Include Open Graph and Twitter metadata
  • ✅ DO: Set appropriate robots directives

Examples

typescript
// Static metadata
export const metadata: Metadata = {
  title: "My App",
  description: "Description of my app",
  openGraph: {
    title: "My App",
    description: "Description",
    images: ["/og-image.png"],
  },
};

// Dynamic metadata
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],
    },
  };
}