Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:46:49 +08:00
commit 60676216e1
9 changed files with 2978 additions and 0 deletions

View File

@@ -0,0 +1,589 @@
# Next.js Best Practices (2025)
Current best practices for Next.js 15/16 development with App Router, Server Components, and modern patterns.
## Framework Versions
### Current Versions
- **Next.js**: 15.x or 16.x (latest stable)
- **React**: 19.x (Server Components, Actions)
- **TypeScript**: 5.x
- **Node.js**: 20 LTS or higher
### Deprecated Versions to Flag
- Next.js 12.x or older (Pages Router era)
- React 17.x or older
- TypeScript 4.x or older
## App Router vs Pages Router
### [OK] Modern (App Router)
```
app/
├── layout.tsx # Root layout
├── page.tsx # Homepage
├── entities/
│ ├── layout.tsx # Nested layout
│ ├── page.tsx # Entity list
│ └── [id]/
│ └── page.tsx # Entity detail
└── api/ # API route handlers (minimal)
└── webhook/
└── route.ts # External webhooks only
```
### [ERROR] Deprecated (Pages Router)
```
pages/
├── _app.tsx # Old pattern
├── _document.tsx # Old pattern
├── index.tsx # Old pattern
├── entities/
│ ├── index.tsx
│ └── [id].tsx
└── api/ # Old API routes
└── entities.ts
```
**Migration Check**: Flag any skills using `pages/` directory or `getServerSideProps`
## Server Components vs Client Components
### Default: Server Components
```tsx
// app/entities/page.tsx
// No 'use client' directive = Server Component (default)
import { db } from '@/lib/db'
export default async function EntitiesPage() {
// Direct database access - only in Server Components
const entities = await db.entity.findMany()
return (
<div>
<h1>Entities</h1>
{entities.map(entity => (
<EntityCard key={entity.id} entity={entity} />
))}
</div>
)
}
```
### Client Components (When Needed)
```tsx
// components/EntityForm.tsx
'use client' // Required for hooks, events, browser APIs
import { useState } from 'react'
import { useForm } from 'react-hook-form'
export function EntityForm() {
const [isSubmitting, setIsSubmitting] = useState(false)
const form = useForm()
return <form>...</form>
}
```
**When to use 'use client':**
- useState, useEffect, other hooks
- Event handlers (onClick, onChange)
- Browser APIs (localStorage, window)
- Third-party libraries requiring browser
**Best Practice**: Keep 'use client' boundary as low as possible in component tree
## Data Fetching
### [OK] Modern: Server Components + fetch
```tsx
// app/characters/page.tsx
export default async function CharactersPage() {
// Fetch in Server Component
const characters = await fetch('https://api.example.com/characters', {
next: { revalidate: 3600 } // Cache for 1 hour
}).then(res => res.json())
return <CharacterList characters={characters} />
}
```
### [OK] Modern: Server Components + Database
```tsx
// app/locations/page.tsx
import { db } from '@/lib/db'
export default async function LocationsPage() {
// Direct database access
const locations = await db.location.findMany({
include: { region: true }
})
return <LocationList locations={locations} />
}
```
### [ERROR] Deprecated: getServerSideProps
```tsx
// pages/characters.tsx - OLD PATTERN
export async function getServerSideProps() {
const characters = await fetchCharacters()
return { props: { characters } }
}
```
**Migration Check**: Replace `getServerSideProps`, `getStaticProps`, `getInitialProps` with Server Component async data fetching
## Server Actions
### [OK] Modern: Server Actions for Mutations
```tsx
// app/actions/character.ts
'use server'
import { db } from '@/lib/db'
import { revalidatePath } from 'next/cache'
export async function createCharacter(formData: FormData) {
const name = formData.get('name') as string
const character = await db.character.create({
data: { name }
})
revalidatePath('/characters')
return { success: true, character }
}
```
```tsx
// app/characters/CreateForm.tsx
'use client'
import { createCharacter } from '@/app/actions/character'
export function CreateForm() {
return (
<form action={createCharacter}>
<input name="name" />
<button type="submit">Create</button>
</form>
)
}
```
### [ERROR] Deprecated: API Routes for Simple Mutations
```tsx
// pages/api/characters.ts - OLD PATTERN
export default async function handler(req, res) {
if (req.method === 'POST') {
const character = await createCharacter(req.body)
res.json({ character })
}
}
```
**When to still use API Routes:**
- Webhooks from external services
- OAuth callbacks
- Third-party integrations requiring public endpoints
**Migration Check**: Replace API routes used for form submissions with Server Actions
## Metadata and SEO
### [OK] Modern: generateMetadata
```tsx
// app/characters/[id]/page.tsx
import { Metadata } from 'next'
export async function generateMetadata({ params }): Promise<Metadata> {
const character = await db.character.findUnique({
where: { id: params.id }
})
return {
title: character.name,
description: character.bio,
openGraph: {
title: character.name,
description: character.bio,
images: [character.image],
},
}
}
```
### [ERROR] Deprecated: next/head
```tsx
// OLD PATTERN
import Head from 'next/head'
<Head>
<title>{character.name}</title>
</Head>
```
## Routing and Navigation
### [OK] Modern: Link from next/link
```tsx
import Link from 'next/link'
<Link href="/characters/123">View Character</Link>
```
### [OK] Modern: useRouter from next/navigation
```tsx
'use client'
import { useRouter } from 'next/navigation' // Not 'next/router'!
export function BackButton() {
const router = useRouter()
return <button onClick={() => router.back()}>Back</button>
}
```
### [ERROR] Deprecated: useRouter from next/router
```tsx
// OLD PATTERN
import { useRouter } from 'next/router' // Pages Router only
```
## Loading States
### [OK] Modern: loading.tsx
```tsx
// app/characters/loading.tsx
export default function Loading() {
return <div>Loading characters...</div>
}
```
### [OK] Modern: Suspense Boundaries
```tsx
// app/characters/page.tsx
import { Suspense } from 'react'
export default function Page() {
return (
<Suspense fallback={<LoadingSkeleton />}>
<CharacterList />
</Suspense>
)
}
```
## Error Handling
### [OK] Modern: error.tsx
```tsx
// app/characters/error.tsx
'use client'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
)
}
```
## Caching and Revalidation
### [OK] Modern: Revalidation Strategies
```tsx
// Time-based revalidation
fetch(url, { next: { revalidate: 3600 } }) // 1 hour
// On-demand revalidation
import { revalidatePath } from 'next/cache'
revalidatePath('/characters')
// Tag-based revalidation
fetch(url, { next: { tags: ['characters'] } })
import { revalidateTag } from 'next/cache'
revalidateTag('characters')
// Disable caching
fetch(url, { cache: 'no-store' })
```
## Middleware
### [OK] Modern: middleware.ts
```tsx
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// Auth check
const token = request.cookies.get('token')
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: '/dashboard/:path*',
}
```
## TypeScript Patterns
### [OK] Type-Safe Server Components
```tsx
interface Character {
id: string
name: string
bio: string
}
export default async function CharacterPage({
params,
}: {
params: { id: string }
}) {
const character: Character = await db.character.findUnique({
where: { id: params.id }
})
return <div>{character.name}</div>
}
```
### [OK] Type-Safe Server Actions
```tsx
'use server'
import { z } from 'zod'
const createCharacterSchema = z.object({
name: z.string().min(2),
bio: z.string().max(5000),
})
export async function createCharacter(
data: z.infer<typeof createCharacterSchema>
) {
const validated = createCharacterSchema.parse(data)
// ...
}
```
## Environment Variables
### [OK] Modern: Type-Safe Env
```typescript
// env.mjs
import { createEnv } from "@t3-oss/env-nextjs"
import { z } from "zod"
export const env = createEnv({
server: {
DATABASE_URL: z.string().url(),
NEXTAUTH_SECRET: z.string().min(1),
},
client: {
NEXT_PUBLIC_API_URL: z.string().url(),
},
runtimeEnv: {
DATABASE_URL: process.env.DATABASE_URL,
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
},
})
```
## Configuration
### [OK] Modern: next.config.js
```javascript
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverActions: {
bodySizeLimit: '2mb',
},
},
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'example.com',
},
],
},
}
export default nextConfig
```
## Common Anti-Patterns to Flag
### [ERROR] Fetching in Client Components
```tsx
'use client'
import { useEffect, useState } from 'react'
// BAD: Fetch in client component
export function Characters() {
const [data, setData] = useState([])
useEffect(() => {
fetch('/api/characters').then(r => r.json()).then(setData)
}, [])
return <div>{data.map(...)}</div>
}
```
[OK] **Fix**: Move data fetching to Server Component or use React Query for client-side data management
### [ERROR] Unnecessary 'use client'
```tsx
'use client' // Not needed!
export function StaticCard({ title, description }) {
return (
<div>
<h3>{title}</h3>
<p>{description}</p>
</div>
)
}
```
[OK] **Fix**: Remove 'use client' if component doesn't use hooks, events, or browser APIs
### [ERROR] Mixing Data Fetching Methods
```tsx
// BAD: Using both old and new patterns
export async function getServerSideProps() { ... }
export default async function Page() {
const data = await fetch(...)
}
```
[OK] **Fix**: Use only Server Component data fetching in App Router
## Performance Best Practices
### Image Optimization
```tsx
import Image from 'next/image'
<Image
src="/character.jpg"
alt="Character portrait"
width={800}
height={600}
priority // For LCP images
/>
```
### Font Optimization
```tsx
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
export default function RootLayout({ children }) {
return (
<html lang="en" className={inter.className}>
{children}
</html>
)
}
```
### Code Splitting
```tsx
import dynamic from 'next/dynamic'
const HeavyComponent = dynamic(() => import('@/components/HeavyComponent'), {
loading: () => <p>Loading...</p>,
ssr: false, // Client-only if needed
})
```
## Validation Checklist
When reviewing Next.js skills, check for:
- [ ] Uses App Router (app/ directory), not Pages Router
- [ ] Server Components for data fetching
- [ ] Server Actions for mutations
- [ ] Proper 'use client' boundaries
- [ ] generateMetadata for SEO
- [ ] useRouter from 'next/navigation'
- [ ] Modern caching with fetch options
- [ ] loading.tsx and error.tsx files
- [ ] Type-safe environment variables
- [ ] Image and font optimization
- [ ] No deprecated APIs (getServerSideProps, etc.)
- [ ] Proper middleware usage
- [ ] Correct import paths
## Quick Reference
### Must Use (Modern)
- [OK] `app/` directory
- [OK] Server Components (default)
- [OK] Server Actions ('use server')
- [OK] `generateMetadata`
- [OK] `useRouter` from 'next/navigation'
- [OK] `revalidatePath`/`revalidateTag`
### Must Avoid (Deprecated)
- [ERROR] `pages/` directory
- [ERROR] `getServerSideProps`
- [ERROR] `getStaticProps`
- [ERROR] `getInitialProps`
- [ERROR] API routes for mutations
- [ERROR] `useRouter` from 'next/router'
- [ERROR] `next/head`