329 lines
8.0 KiB
Markdown
329 lines
8.0 KiB
Markdown
---
|
|
name: next-best-practices
|
|
description: Next.js best practices - file conventions, RSC boundaries, data patterns, async APIs, metadata, error handling, route handlers, image/font optimization, bundling
|
|
user-invocable: false
|
|
---
|
|
|
|
# Next.js Best Practices
|
|
|
|
Apply these rules when writing or reviewing Next.js code.
|
|
|
|
## File Conventions
|
|
|
|
See [file-conventions.md](./file-conventions.md) for:
|
|
|
|
- Project structure and special files
|
|
- Route segments (dynamic, catch-all, groups)
|
|
- Parallel and intercepting routes
|
|
- Middleware rename in v16 (middleware → proxy)
|
|
|
|
## RSC Boundaries
|
|
|
|
Detect invalid React Server Component patterns.
|
|
|
|
See [rsc-boundaries.md](./rsc-boundaries.md) for:
|
|
|
|
- Async client component detection (invalid)
|
|
- Non-serializable props detection
|
|
- Server Action exceptions
|
|
|
|
## Async Patterns
|
|
|
|
Next.js 15+ async API changes.
|
|
|
|
See [async-patterns.md](./async-patterns.md) for:
|
|
|
|
- Async `params` and `searchParams`
|
|
- Async `cookies()` and `headers()`
|
|
- Migration codemod
|
|
|
|
## Runtime Selection
|
|
|
|
See [runtime-selection.md](./runtime-selection.md) for:
|
|
|
|
- Default to Node.js runtime
|
|
- When Edge runtime is appropriate
|
|
|
|
## Directives
|
|
|
|
See [directives.md](./directives.md) for:
|
|
|
|
- `'use client'`, `'use server'` (React)
|
|
- `'use cache'` (Next.js)
|
|
|
|
## Functions
|
|
|
|
See [functions.md](./functions.md) for:
|
|
|
|
- Navigation hooks: `useRouter`, `usePathname`, `useSearchParams`, `useParams`
|
|
- Server functions: `cookies`, `headers`, `draftMode`, `after`
|
|
- Generate functions: `generateStaticParams`, `generateMetadata`
|
|
|
|
## Error Handling
|
|
|
|
See [error-handling.md](./error-handling.md) for:
|
|
|
|
- `error.tsx`, `global-error.tsx`, `not-found.tsx`
|
|
- `redirect`, `permanentRedirect`, `notFound`
|
|
- `forbidden`, `unauthorized` (auth errors)
|
|
- `unstable_rethrow` for catch blocks
|
|
|
|
## Data Patterns
|
|
|
|
Project-specific: See [uuid-handling.md](./uuid-handling.md) for ADR-019 UUID handling patterns.
|
|
|
|
See [data-patterns.md](./data-patterns.md) for:
|
|
|
|
- Server Components vs Server Actions vs Route Handlers
|
|
- Avoiding data waterfalls (`Promise.all`, Suspense, preload)
|
|
- Client component data fetching
|
|
|
|
## Route Handlers
|
|
|
|
See [route-handlers.md](./route-handlers.md) for:
|
|
|
|
- `route.ts` basics
|
|
- GET handler conflicts with `page.tsx`
|
|
- Environment behavior (no React DOM)
|
|
- When to use vs Server Actions
|
|
|
|
## Metadata & OG Images
|
|
|
|
See [metadata.md](./metadata.md) for:
|
|
|
|
- Static and dynamic metadata
|
|
- `generateMetadata` function
|
|
- OG image generation with `next/og`
|
|
- File-based metadata conventions
|
|
|
|
## Image Optimization
|
|
|
|
See [image.md](./image.md) for:
|
|
|
|
- Always use `next/image` over `<img>`
|
|
- Remote images configuration
|
|
- Responsive `sizes` attribute
|
|
- Blur placeholders
|
|
- Priority loading for LCP
|
|
|
|
## Font Optimization
|
|
|
|
See [font.md](./font.md) for:
|
|
|
|
- `next/font` setup
|
|
- Google Fonts, local fonts
|
|
- Tailwind CSS integration
|
|
- Preloading subsets
|
|
|
|
## Bundling
|
|
|
|
See [bundling.md](./bundling.md) for:
|
|
|
|
- Server-incompatible packages
|
|
- CSS imports (not link tags)
|
|
- Polyfills (already included)
|
|
- ESM/CommonJS issues
|
|
- Bundle analysis
|
|
|
|
## Scripts
|
|
|
|
See [scripts.md](./scripts.md) for:
|
|
|
|
- `next/script` vs native script tags
|
|
- Inline scripts need `id`
|
|
- Loading strategies
|
|
- Google Analytics with `@next/third-parties`
|
|
|
|
## Hydration Errors
|
|
|
|
See [hydration-error.md](./hydration-error.md) for:
|
|
|
|
- Common causes (browser APIs, dates, invalid HTML)
|
|
- Debugging with error overlay
|
|
- Fixes for each cause
|
|
|
|
## Suspense Boundaries
|
|
|
|
See [suspense-boundaries.md](./suspense-boundaries.md) for:
|
|
|
|
- CSR bailout with `useSearchParams` and `usePathname`
|
|
- Which hooks require Suspense boundaries
|
|
|
|
## Parallel & Intercepting Routes
|
|
|
|
See [parallel-routes.md](./parallel-routes.md) for:
|
|
|
|
- Modal patterns with `@slot` and `(.)` interceptors
|
|
- `default.tsx` for fallbacks
|
|
- Closing modals correctly with `router.back()`
|
|
|
|
## Self-Hosting
|
|
|
|
See [self-hosting.md](./self-hosting.md) for:
|
|
|
|
- `output: 'standalone'` for Docker
|
|
- Cache handlers for multi-instance ISR
|
|
- What works vs needs extra setup
|
|
|
|
## NAP-DMS Project-Specific Rules (MUST FOLLOW)
|
|
|
|
These rules are mandatory for the NAP-DMS LCBP3 frontend project:
|
|
|
|
### State Management (บังคับใช้)
|
|
|
|
**Server State - TanStack Query (React Query)**
|
|
|
|
```tsx
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
|
|
// ❌ ห้ามใช้ useEffect โดยตรง
|
|
// ✅ ใช้ TanStack Query
|
|
export function useCorrespondences(projectId: string) {
|
|
return useQuery({
|
|
queryKey: ['correspondences', projectId],
|
|
queryFn: () => correspondenceService.getAll(projectId),
|
|
staleTime: 5 * 60 * 1000,
|
|
});
|
|
}
|
|
```
|
|
|
|
**Form State - React Hook Form + Zod**
|
|
|
|
```tsx
|
|
import { useForm } from 'react-hook-form';
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import * as z from 'zod';
|
|
|
|
const schema = z.object({
|
|
title: z.string().min(1, 'กรุณาระบุหัวเรื่อง'),
|
|
projectUuid: z.string().uuid('กรุณาเลือกโปรเจกต์'),
|
|
});
|
|
|
|
const form = useForm({
|
|
resolver: zodResolver(schema),
|
|
});
|
|
```
|
|
|
|
### ADR-019 UUID Handling (CRITICAL)
|
|
|
|
```tsx
|
|
// Interface ต้องมีทั้ง id และ publicId
|
|
interface Contract {
|
|
id?: number; // Internal (อาจ undefined)
|
|
publicId?: string; // UUID - ใช้ตัวนี้
|
|
contractCode: string;
|
|
}
|
|
|
|
// Select options - ใช้ pattern นี้เสมอ
|
|
const options = contracts.map((c) => ({
|
|
label: `${c.contractName} (${c.contractCode})`,
|
|
value: String(c.publicId ?? c.id ?? ''), // fallback pattern
|
|
key: String(c.publicId ?? c.id ?? ''),
|
|
}));
|
|
|
|
// ❌ ห้ามใช้ parseInt บน UUID
|
|
// const id = parseInt(projectId); // WRONG!
|
|
|
|
// ✅ ส่ง UUID string ตรงๆ
|
|
apiClient.get(`/projects/${projectId}`); // projectId is UUID string
|
|
```
|
|
|
|
### Naming Conventions
|
|
|
|
**Code Identifiers - ภาษาอังกฤษ**
|
|
|
|
```tsx
|
|
// ✅ Correct
|
|
interface Correspondence {
|
|
documentNumber: string;
|
|
createdAt: string;
|
|
}
|
|
|
|
// ❌ Wrong
|
|
interface เอกสาร {
|
|
เลขที่: string;
|
|
}
|
|
```
|
|
|
|
**Comments - ภาษาไทย**
|
|
|
|
```tsx
|
|
// ✅ Correct - อธิบาย logic เป็นภาษาไทย
|
|
// ตรวจสอบว่ามีการระบุ projectUuid หรือไม่
|
|
if (!data.projectUuid) {
|
|
throw new Error('กรุณาเลือกโปรเจกต์');
|
|
}
|
|
|
|
// ❌ Wrong - ห้ามใช้ภาษาอังกฤษใน comments
|
|
// Check if projectUuid is provided
|
|
```
|
|
|
|
### UI Components
|
|
|
|
**บังคับใช้ shadcn/ui**
|
|
|
|
```tsx
|
|
// ✅ Correct
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
|
|
// ❌ Wrong - ไม่สร้าง component เองถ้ามีใน shadcn
|
|
const MyButton = () => <button className="...">Click</button>;
|
|
```
|
|
|
|
### File Upload Pattern
|
|
|
|
```tsx
|
|
import { useDropzone } from 'react-dropzone';
|
|
|
|
// Two-phase upload
|
|
const onDrop = useCallback(async (files: File[]) => {
|
|
// Phase 1: Upload to temp
|
|
const tempFiles = await Promise.all(files.map((file) => uploadService.uploadTemp(file)));
|
|
setTempIds(tempFiles.map((f) => f.tempId));
|
|
}, []);
|
|
|
|
// Phase 2: Commit on form submit
|
|
const onSubmit = async (data: FormData) => {
|
|
await correspondenceService.create({
|
|
...data,
|
|
tempFileIds,
|
|
});
|
|
};
|
|
```
|
|
|
|
### API Client Setup
|
|
|
|
```typescript
|
|
// lib/api/client.ts
|
|
const apiClient = axios.create({
|
|
baseURL: process.env.NEXT_PUBLIC_API_URL,
|
|
timeout: 30000,
|
|
});
|
|
|
|
// Auto-add Idempotency-Key
|
|
apiClient.interceptors.request.use((config) => {
|
|
if (['post', 'put', 'patch'].includes(config.method?.toLowerCase() || '')) {
|
|
config.headers['Idempotency-Key'] = uuidv4();
|
|
}
|
|
return config;
|
|
});
|
|
```
|
|
|
|
### Anti-Patterns (ห้ามทำ)
|
|
|
|
- ❌ Fetch data ใน useEffect โดยตรง
|
|
- ❌ Props drilling ลึกเกิน 3 levels
|
|
- ❌ Inline styles (ใช้ Tailwind)
|
|
- ❌ console.log ใน production
|
|
- ❌ parseInt() บน UUID values
|
|
- ❌ ใช้ index เป็น key ใน list
|
|
- ❌ Snake_case ใน form field names (ใช้ camelCase)
|
|
|
|
---
|
|
|
|
See [debug-tricks.md](./debug-tricks.md) for:
|
|
|
|
- MCP endpoint for AI-assisted debugging
|
|
- Rebuild specific routes with `--debug-build-paths`
|