Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "nextjs",
|
||||
"description": "Build Next.js 16 apps with App Router, Server Components/Actions, Cache Components (use cache), and async route params. Includes proxy.ts (replaces middleware.ts) and React 19.2. Use when: building Next.js 16 projects, or troubleshooting async params (Promise types), use cache directives, parallel route 404s (missing default.js), or proxy.ts CORS.",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Jeremy Dawes",
|
||||
"email": "jeremy@jezweb.net"
|
||||
},
|
||||
"skills": [
|
||||
"./"
|
||||
]
|
||||
}
|
||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# nextjs
|
||||
|
||||
Build Next.js 16 apps with App Router, Server Components/Actions, Cache Components (use cache), and async route params. Includes proxy.ts (replaces middleware.ts) and React 19.2. Use when: building Next.js 16 projects, or troubleshooting async params (Promise types), use cache directives, parallel route 404s (missing default.js), or proxy.ts CORS.
|
||||
85
plugin.lock.json
Normal file
85
plugin.lock.json
Normal file
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||
"pluginId": "gh:jezweb/claude-skills:skills/nextjs",
|
||||
"normalized": {
|
||||
"repo": null,
|
||||
"ref": "refs/tags/v20251128.0",
|
||||
"commit": "7752a82e83393fe7be5bce05d0911b6498c93050",
|
||||
"treeHash": "751d0e9f13f37b25b60eb7bd37981fed86ad120f2e150e053f240a521b9b9e85",
|
||||
"generatedAt": "2025-11-28T10:18:59.854318Z",
|
||||
"toolVersion": "publish_plugins.py@0.2.0"
|
||||
},
|
||||
"origin": {
|
||||
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||
"branch": "master",
|
||||
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||
},
|
||||
"manifest": {
|
||||
"name": "nextjs",
|
||||
"description": "Build Next.js 16 apps with App Router, Server Components/Actions, Cache Components (use cache), and async route params. Includes proxy.ts (replaces middleware.ts) and React 19.2. Use when: building Next.js 16 projects, or troubleshooting async params (Promise types), use cache directives, parallel route 404s (missing default.js), or proxy.ts CORS.",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"content": {
|
||||
"files": [
|
||||
{
|
||||
"path": "README.md",
|
||||
"sha256": "291f3e1170756b73e817589cfd801423a4db65b30f736b90173f1b0464d3c688"
|
||||
},
|
||||
{
|
||||
"path": "SKILL.md",
|
||||
"sha256": "0d274baa9085ed5ded3c5fca56c8498352197345eb18136d125e05e1ecee9164"
|
||||
},
|
||||
{
|
||||
"path": "references/next-16-migration-guide.md",
|
||||
"sha256": "b13cb9e7d265f5eeb49f4d25bbc3db9f4afaec519adfb3661a94e811fd41d4a7"
|
||||
},
|
||||
{
|
||||
"path": "references/top-errors.md",
|
||||
"sha256": "fbc231577f65325a341578a53faa59bcefb0158ab39053d02b8245a09dd7ab05"
|
||||
},
|
||||
{
|
||||
"path": "scripts/check-versions.sh",
|
||||
"sha256": "81b37d00e6f3b91f2b9e411dd85f5f9681c95957eb951ed8d9da9da5914f5a70"
|
||||
},
|
||||
{
|
||||
"path": ".claude-plugin/plugin.json",
|
||||
"sha256": "c1c2ed4e43352267df00c2ba4037ec218053613a8bb4a8eb93130c2ad1bf6f8b"
|
||||
},
|
||||
{
|
||||
"path": "templates/server-actions-form.tsx",
|
||||
"sha256": "8b13071ca8c839e1e7a37792686a25853f43ecfdbb17cf4534c4489049ee57cb"
|
||||
},
|
||||
{
|
||||
"path": "templates/proxy-migration.ts",
|
||||
"sha256": "362c27846d847e2ebd01692e980f1b3c0b4bb4e1cff8f1fd98e6979ee59dbb3c"
|
||||
},
|
||||
{
|
||||
"path": "templates/route-handler-api.ts",
|
||||
"sha256": "be6533d939727add7e97aaebf6b91eed846a8c343b93f9a6b27c44019811036d"
|
||||
},
|
||||
{
|
||||
"path": "templates/package.json",
|
||||
"sha256": "298748ae5473db8d20cae62bba914fd6878f983cd2ebf0c55f6ae3a50b264853"
|
||||
},
|
||||
{
|
||||
"path": "templates/app-router-async-params.tsx",
|
||||
"sha256": "7630604e4d4d11e83a36b2337f833c286ef1381ca5217076a15db8ec32a856c5"
|
||||
},
|
||||
{
|
||||
"path": "templates/cache-component-use-cache.tsx",
|
||||
"sha256": "986256a04ebce7de7c624a5c5d0f5fd84fc2a590cc5600dfff18a97bae8eabae"
|
||||
},
|
||||
{
|
||||
"path": "templates/parallel-routes-with-default.tsx",
|
||||
"sha256": "513cac57ae53e33fd619cf15876320264fe93d366727426bef09549ceae2e541"
|
||||
}
|
||||
],
|
||||
"dirSha256": "751d0e9f13f37b25b60eb7bd37981fed86ad120f2e150e053f240a521b9b9e85"
|
||||
},
|
||||
"security": {
|
||||
"scannedAt": null,
|
||||
"scannerVersion": null,
|
||||
"flags": []
|
||||
}
|
||||
}
|
||||
647
references/next-16-migration-guide.md
Normal file
647
references/next-16-migration-guide.md
Normal file
@@ -0,0 +1,647 @@
|
||||
# Next.js 16 Migration Guide
|
||||
|
||||
**From**: Next.js 15.x
|
||||
**To**: Next.js 16.0.0
|
||||
**Last Updated**: 2025-10-24
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Breaking Changes](#breaking-changes)
|
||||
3. [New Features](#new-features)
|
||||
4. [Migration Steps](#migration-steps)
|
||||
5. [Automated Migration](#automated-migration)
|
||||
6. [Manual Migration](#manual-migration)
|
||||
7. [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Next.js 16 introduces significant changes:
|
||||
|
||||
- **Breaking Changes**: 6 major breaking changes
|
||||
- **New Features**: Cache Components, updated caching APIs, React 19.2
|
||||
- **Performance**: Turbopack stable, 2–5× faster builds
|
||||
- **Migration Time**: ~1-2 hours for medium-sized apps
|
||||
|
||||
**Recommendation**: Use automated codemod first, then manually fix remaining issues.
|
||||
|
||||
---
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
### 1. Async Route Parameters ⚠️
|
||||
|
||||
**What Changed**: `params`, `searchParams`, `cookies()`, `headers()`, `draftMode()` are now async.
|
||||
|
||||
**Before** (Next.js 15):
|
||||
```typescript
|
||||
export default function Page({ params, searchParams }) {
|
||||
const slug = params.slug
|
||||
const query = searchParams.q
|
||||
}
|
||||
```
|
||||
|
||||
**After** (Next.js 16):
|
||||
```typescript
|
||||
export default async function Page({ params, searchParams }) {
|
||||
const { slug } = await params
|
||||
const { q: query } = await searchParams
|
||||
}
|
||||
```
|
||||
|
||||
**TypeScript Types**:
|
||||
```typescript
|
||||
// Before
|
||||
type PageProps = {
|
||||
params: { slug: string }
|
||||
searchParams: { q: string }
|
||||
}
|
||||
|
||||
// After
|
||||
type PageProps = {
|
||||
params: Promise<{ slug: string }>
|
||||
searchParams: Promise<{ q: string }>
|
||||
}
|
||||
```
|
||||
|
||||
**Fix**:
|
||||
1. Add `async` to function
|
||||
2. Add `await` before params/searchParams
|
||||
3. Update TypeScript types to `Promise<>`
|
||||
|
||||
---
|
||||
|
||||
### 2. Middleware → Proxy ⚠️
|
||||
|
||||
**What Changed**: `middleware.ts` is deprecated. Use `proxy.ts` instead.
|
||||
|
||||
**Migration**:
|
||||
```bash
|
||||
# 1. Rename file
|
||||
mv middleware.ts proxy.ts
|
||||
|
||||
# 2. Rename function in file
|
||||
# middleware → proxy
|
||||
```
|
||||
|
||||
**Before** (middleware.ts):
|
||||
```typescript
|
||||
export function middleware(request: NextRequest) {
|
||||
return NextResponse.next()
|
||||
}
|
||||
```
|
||||
|
||||
**After** (proxy.ts):
|
||||
```typescript
|
||||
export function proxy(request: NextRequest) {
|
||||
return NextResponse.next()
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: `middleware.ts` still works in Next.js 16 but is deprecated.
|
||||
|
||||
---
|
||||
|
||||
### 3. Parallel Routes Require `default.js` ⚠️
|
||||
|
||||
**What Changed**: All parallel routes now REQUIRE explicit `default.js` files.
|
||||
|
||||
**Before** (Next.js 15):
|
||||
```
|
||||
app/
|
||||
├── @modal/
|
||||
│ └── login/
|
||||
│ └── page.tsx
|
||||
```
|
||||
|
||||
**After** (Next.js 16):
|
||||
```
|
||||
app/
|
||||
├── @modal/
|
||||
│ ├── login/
|
||||
│ │ └── page.tsx
|
||||
│ └── default.tsx ← REQUIRED
|
||||
```
|
||||
|
||||
**default.tsx**:
|
||||
```typescript
|
||||
export default function ModalDefault() {
|
||||
return null
|
||||
}
|
||||
```
|
||||
|
||||
**Fix**: Add `default.tsx` to every `@folder` in parallel routes.
|
||||
|
||||
---
|
||||
|
||||
### 4. Removed Features ⚠️
|
||||
|
||||
**Removed**:
|
||||
- AMP support
|
||||
- `next lint` command
|
||||
- `serverRuntimeConfig` and `publicRuntimeConfig`
|
||||
- `experimental.ppr` flag
|
||||
- Automatic `scroll-behavior: smooth`
|
||||
- Node.js 18 support
|
||||
|
||||
**Migration**:
|
||||
|
||||
**AMP**:
|
||||
```typescript
|
||||
// Before
|
||||
export const config = { amp: true }
|
||||
|
||||
// After
|
||||
// No direct replacement - use separate AMP pages or frameworks
|
||||
```
|
||||
|
||||
**Linting**:
|
||||
```bash
|
||||
# Before
|
||||
npm run lint
|
||||
|
||||
# After
|
||||
npx eslint .
|
||||
# or
|
||||
npx biome lint .
|
||||
```
|
||||
|
||||
**Runtime Config**:
|
||||
```typescript
|
||||
// Before
|
||||
module.exports = {
|
||||
serverRuntimeConfig: { secret: 'abc' },
|
||||
publicRuntimeConfig: { apiUrl: 'https://api' },
|
||||
}
|
||||
|
||||
// After
|
||||
// Use environment variables
|
||||
process.env.SECRET
|
||||
process.env.NEXT_PUBLIC_API_URL
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Version Requirements ⚠️
|
||||
|
||||
**Minimum Versions**:
|
||||
- Node.js: 20.9+ (Node 18 removed)
|
||||
- TypeScript: 5.1+
|
||||
- React: 19.2+
|
||||
- Browsers: Chrome 111+, Safari 16.4+, Firefox 109+
|
||||
|
||||
**Upgrade Node.js**:
|
||||
```bash
|
||||
# Check current version
|
||||
node --version
|
||||
|
||||
# Upgrade (using nvm)
|
||||
nvm install 20
|
||||
nvm use 20
|
||||
nvm alias default 20
|
||||
|
||||
# Verify
|
||||
node --version # Should be 20.9+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. Image Defaults Changed ⚠️
|
||||
|
||||
**What Changed**: `next/image` default settings changed.
|
||||
|
||||
| Setting | Next.js 15 | Next.js 16 |
|
||||
|---------|------------|------------|
|
||||
| TTL | 60s | 4 hours |
|
||||
| imageSizes | 8 sizes | 5 sizes |
|
||||
| qualities | 3 qualities | 1 quality (75) |
|
||||
|
||||
**Impact**: Images cache longer, fewer sizes generated.
|
||||
|
||||
**Revert** (if needed):
|
||||
```typescript
|
||||
// next.config.ts
|
||||
const config = {
|
||||
images: {
|
||||
minimumCacheTTL: 60, // Revert to 60 seconds
|
||||
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], // Old sizes
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## New Features
|
||||
|
||||
### 1. Cache Components ✨
|
||||
|
||||
**Opt-in caching** with `"use cache"` directive.
|
||||
|
||||
**Before** (Next.js 15 - implicit caching):
|
||||
```typescript
|
||||
// All Server Components cached by default
|
||||
export async function MyComponent() {
|
||||
const data = await fetch('/api/data')
|
||||
return <div>{data.value}</div>
|
||||
}
|
||||
```
|
||||
|
||||
**After** (Next.js 16 - opt-in):
|
||||
```typescript
|
||||
// NOT cached by default
|
||||
export async function MyComponent() {
|
||||
const data = await fetch('/api/data')
|
||||
return <div>{data.value}</div>
|
||||
}
|
||||
|
||||
// Opt-in to caching
|
||||
'use cache'
|
||||
export async function CachedComponent() {
|
||||
const data = await fetch('/api/data')
|
||||
return <div>{data.value}</div>
|
||||
}
|
||||
```
|
||||
|
||||
**See**: `references/cache-components-guide.md`
|
||||
|
||||
---
|
||||
|
||||
### 2. Updated Caching APIs ✨
|
||||
|
||||
**`revalidateTag()` now requires 2 arguments**:
|
||||
|
||||
**Before**:
|
||||
```typescript
|
||||
revalidateTag('posts')
|
||||
```
|
||||
|
||||
**After**:
|
||||
```typescript
|
||||
revalidateTag('posts', 'max') // Second argument required
|
||||
```
|
||||
|
||||
**New APIs**:
|
||||
- `updateTag()` - Immediate refresh (read-your-writes)
|
||||
- `refresh()` - Refresh uncached data only
|
||||
|
||||
---
|
||||
|
||||
### 3. React 19.2 Integration ✨
|
||||
|
||||
**New React features**:
|
||||
- View Transitions
|
||||
- `useEffectEvent()` (experimental)
|
||||
- React Compiler (stable)
|
||||
|
||||
**See**: `references/react-19-integration.md`
|
||||
|
||||
---
|
||||
|
||||
### 4. Turbopack Stable ✨
|
||||
|
||||
**Default bundler**: Turbopack is now stable and default.
|
||||
|
||||
**Metrics**:
|
||||
- 2–5× faster production builds
|
||||
- Up to 10× faster Fast Refresh
|
||||
|
||||
**Opt-out** (if incompatible):
|
||||
```bash
|
||||
npm run build -- --webpack
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Steps
|
||||
|
||||
### Step 1: Prerequisites
|
||||
|
||||
1. **Backup your project**:
|
||||
```bash
|
||||
git commit -am "Pre-migration checkpoint"
|
||||
```
|
||||
|
||||
2. **Check Node.js version**:
|
||||
```bash
|
||||
node --version # Should be 20.9+
|
||||
```
|
||||
|
||||
3. **Update dependencies**:
|
||||
```bash
|
||||
npm install next@16 react@19.2 react-dom@19.2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: Run Automated Codemod
|
||||
|
||||
```bash
|
||||
npx @next/codemod@canary upgrade latest
|
||||
```
|
||||
|
||||
**What it fixes**:
|
||||
- ✅ Async params (adds `await`)
|
||||
- ✅ Async searchParams
|
||||
- ✅ Async cookies()
|
||||
- ✅ Async headers()
|
||||
- ✅ Updates TypeScript types
|
||||
|
||||
**What it does NOT fix**:
|
||||
- ❌ middleware.ts → proxy.ts (manual)
|
||||
- ❌ Parallel routes default.js (manual)
|
||||
- ❌ Removed features (manual)
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Manual Fixes
|
||||
|
||||
#### Fix 1: Migrate middleware.ts → proxy.ts
|
||||
|
||||
```bash
|
||||
# Rename file
|
||||
mv middleware.ts proxy.ts
|
||||
|
||||
# Update function name
|
||||
# middleware → proxy
|
||||
```
|
||||
|
||||
#### Fix 2: Add default.js to Parallel Routes
|
||||
|
||||
```bash
|
||||
# For each @folder, create default.tsx
|
||||
touch app/@modal/default.tsx
|
||||
touch app/@feed/default.tsx
|
||||
```
|
||||
|
||||
```typescript
|
||||
// app/@modal/default.tsx
|
||||
export default function ModalDefault() {
|
||||
return null
|
||||
}
|
||||
```
|
||||
|
||||
#### Fix 3: Replace Removed Features
|
||||
|
||||
**AMP**: Remove AMP config or migrate to separate AMP implementation.
|
||||
|
||||
**Linting**: Update scripts in `package.json`:
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"lint": "eslint ."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Runtime Config**: Use environment variables.
|
||||
|
||||
---
|
||||
|
||||
### Step 4: Update Caching
|
||||
|
||||
**Migrate from implicit to explicit caching**:
|
||||
|
||||
1. Find Server Components with expensive operations
|
||||
2. Add `"use cache"` directive
|
||||
3. Update `revalidateTag()` calls to include `cacheLife`
|
||||
|
||||
**Example**:
|
||||
```typescript
|
||||
// Before
|
||||
export async function ExpensiveComponent() {
|
||||
const data = await fetch('/api/data') // Cached implicitly
|
||||
return <div>{data.value}</div>
|
||||
}
|
||||
|
||||
// After
|
||||
'use cache'
|
||||
export async function ExpensiveComponent() {
|
||||
const data = await fetch('/api/data') // Cached explicitly
|
||||
return <div>{data.value}</div>
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 5: Test
|
||||
|
||||
```bash
|
||||
# Development
|
||||
npm run dev
|
||||
|
||||
# Production build
|
||||
npm run build
|
||||
|
||||
# Check for errors
|
||||
npm run type-check
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 6: Update CI/CD
|
||||
|
||||
**Update Node.js version** in CI config:
|
||||
|
||||
**.github/workflows/ci.yml**:
|
||||
```yaml
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.9' # Update from 18
|
||||
```
|
||||
|
||||
**Dockerfile**:
|
||||
```dockerfile
|
||||
FROM node:20.9-alpine # Update from node:18
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Automated Migration
|
||||
|
||||
**Codemod** (recommended):
|
||||
```bash
|
||||
npx @next/codemod@canary upgrade latest
|
||||
```
|
||||
|
||||
**Options**:
|
||||
- `--dry` - Preview changes without applying
|
||||
- `--force` - Skip confirmation prompts
|
||||
|
||||
**What it migrates**:
|
||||
1. ✅ Async params
|
||||
2. ✅ Async searchParams
|
||||
3. ✅ Async cookies()
|
||||
4. ✅ Async headers()
|
||||
5. ✅ TypeScript types
|
||||
|
||||
**Manual steps after codemod**:
|
||||
1. Rename middleware.ts → proxy.ts
|
||||
2. Add default.js to parallel routes
|
||||
3. Replace removed features
|
||||
4. Update caching patterns
|
||||
|
||||
---
|
||||
|
||||
## Manual Migration
|
||||
|
||||
If codemod fails or you prefer manual migration:
|
||||
|
||||
### 1. Async Params
|
||||
|
||||
**Find**:
|
||||
```bash
|
||||
grep -r "params\." app/
|
||||
grep -r "searchParams\." app/
|
||||
```
|
||||
|
||||
**Replace**:
|
||||
```typescript
|
||||
// Before
|
||||
const slug = params.slug
|
||||
|
||||
// After
|
||||
const { slug } = await params
|
||||
```
|
||||
|
||||
### 2. Async Cookies/Headers
|
||||
|
||||
**Find**:
|
||||
```bash
|
||||
grep -r "cookies()" app/
|
||||
grep -r "headers()" app/
|
||||
```
|
||||
|
||||
**Replace**:
|
||||
```typescript
|
||||
// Before
|
||||
const cookieStore = cookies()
|
||||
|
||||
// After
|
||||
const cookieStore = await cookies()
|
||||
```
|
||||
|
||||
### 3. TypeScript Types
|
||||
|
||||
**Find**: All `PageProps` types
|
||||
|
||||
**Replace**:
|
||||
```typescript
|
||||
// Before
|
||||
type PageProps = {
|
||||
params: { id: string }
|
||||
searchParams: { q: string }
|
||||
}
|
||||
|
||||
// After
|
||||
type PageProps = {
|
||||
params: Promise<{ id: string }>
|
||||
searchParams: Promise<{ q: string }>
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Error: `params` is a Promise
|
||||
|
||||
**Cause**: Not awaiting params in Next.js 16.
|
||||
|
||||
**Fix**:
|
||||
```typescript
|
||||
// ❌ Before
|
||||
const id = params.id
|
||||
|
||||
// ✅ After
|
||||
const { id } = await params
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Error: Parallel route missing `default.js`
|
||||
|
||||
**Cause**: Next.js 16 requires `default.js` for all parallel routes.
|
||||
|
||||
**Fix**: Create `default.tsx`:
|
||||
```typescript
|
||||
// app/@modal/default.tsx
|
||||
export default function ModalDefault() {
|
||||
return null
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Error: `revalidateTag` requires 2 arguments
|
||||
|
||||
**Cause**: `revalidateTag()` API changed in Next.js 16.
|
||||
|
||||
**Fix**:
|
||||
```typescript
|
||||
// ❌ Before
|
||||
revalidateTag('posts')
|
||||
|
||||
// ✅ After
|
||||
revalidateTag('posts', 'max')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Error: Turbopack build failure
|
||||
|
||||
**Cause**: Turbopack is now default in Next.js 16.
|
||||
|
||||
**Fix**: Opt-out if incompatible:
|
||||
```bash
|
||||
npm run build -- --webpack
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Error: Node.js version too old
|
||||
|
||||
**Cause**: Next.js 16 requires Node.js 20.9+.
|
||||
|
||||
**Fix**: Upgrade Node.js:
|
||||
```bash
|
||||
nvm install 20
|
||||
nvm use 20
|
||||
nvm alias default 20
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Checklist
|
||||
|
||||
- [ ] Backup project (git commit)
|
||||
- [ ] Check Node.js version (20.9+)
|
||||
- [ ] Update dependencies (`npm install next@16 react@19.2 react-dom@19.2`)
|
||||
- [ ] Run codemod (`npx @next/codemod@canary upgrade latest`)
|
||||
- [ ] Rename middleware.ts → proxy.ts
|
||||
- [ ] Add default.js to parallel routes
|
||||
- [ ] Remove AMP config (if used)
|
||||
- [ ] Replace runtime config with env vars
|
||||
- [ ] Update `revalidateTag()` calls (add `cacheLife`)
|
||||
- [ ] Add `"use cache"` where needed
|
||||
- [ ] Test dev server (`npm run dev`)
|
||||
- [ ] Test production build (`npm run build`)
|
||||
- [ ] Update CI/CD Node.js version
|
||||
- [ ] Deploy to staging
|
||||
- [ ] Deploy to production
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
- **Next.js 16 Blog**: https://nextjs.org/blog/next-16
|
||||
- **Codemod**: `npx @next/codemod@canary upgrade latest`
|
||||
- **Templates**: See `templates/` directory
|
||||
- **Common Errors**: See `references/top-errors.md`
|
||||
|
||||
---
|
||||
|
||||
**Migration Support**: jeremy@jezweb.net
|
||||
550
references/top-errors.md
Normal file
550
references/top-errors.md
Normal file
@@ -0,0 +1,550 @@
|
||||
# Next.js 16 - Top 18 Errors & Solutions
|
||||
|
||||
**Last Updated**: 2025-10-24
|
||||
**Prevention Rate**: 100% (all documented errors caught)
|
||||
|
||||
This guide covers the 18 most common errors when using Next.js 16 and their solutions.
|
||||
|
||||
---
|
||||
|
||||
## Error #1: `params` is a Promise
|
||||
|
||||
**Error Message**:
|
||||
```
|
||||
Type 'Promise<{ id: string }>' is not assignable to type '{ id: string }'
|
||||
```
|
||||
|
||||
**Cause**: Next.js 16 changed `params` to async.
|
||||
|
||||
**Solution**:
|
||||
```typescript
|
||||
// ❌ Before (Next.js 15)
|
||||
export default function Page({ params }: { params: { id: string } }) {
|
||||
const id = params.id
|
||||
}
|
||||
|
||||
// ✅ After (Next.js 16)
|
||||
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params
|
||||
}
|
||||
```
|
||||
|
||||
**TypeScript Fix**:
|
||||
```typescript
|
||||
type Params<T = Record<string, string>> = Promise<T>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error #2: `searchParams` is a Promise
|
||||
|
||||
**Error Message**:
|
||||
```
|
||||
Property 'query' does not exist on type 'Promise<{ query: string }>'
|
||||
```
|
||||
|
||||
**Cause**: `searchParams` is now async in Next.js 16.
|
||||
|
||||
**Solution**:
|
||||
```typescript
|
||||
// ❌ Before
|
||||
export default function Page({ searchParams }: { searchParams: { q: string } }) {
|
||||
const query = searchParams.q
|
||||
}
|
||||
|
||||
// ✅ After
|
||||
export default async function Page({ searchParams }: { searchParams: Promise<{ q: string }> }) {
|
||||
const { q: query } = await searchParams
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error #3: `cookies()` requires await
|
||||
|
||||
**Error Message**:
|
||||
```
|
||||
'cookies' implicitly has return type 'any'
|
||||
```
|
||||
|
||||
**Cause**: `cookies()` is async in Next.js 16.
|
||||
|
||||
**Solution**:
|
||||
```typescript
|
||||
// ❌ Before
|
||||
import { cookies } from 'next/headers'
|
||||
|
||||
export function MyComponent() {
|
||||
const cookieStore = cookies()
|
||||
const token = cookieStore.get('token')
|
||||
}
|
||||
|
||||
// ✅ After
|
||||
import { cookies } from 'next/headers'
|
||||
|
||||
export async function MyComponent() {
|
||||
const cookieStore = await cookies()
|
||||
const token = cookieStore.get('token')
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error #4: `headers()` requires await
|
||||
|
||||
**Error Message**:
|
||||
```
|
||||
'headers' implicitly has return type 'any'
|
||||
```
|
||||
|
||||
**Cause**: `headers()` is async in Next.js 16.
|
||||
|
||||
**Solution**:
|
||||
```typescript
|
||||
// ❌ Before
|
||||
import { headers } from 'next/headers'
|
||||
|
||||
export function MyComponent() {
|
||||
const headersList = headers()
|
||||
}
|
||||
|
||||
// ✅ After
|
||||
import { headers } from 'next/headers'
|
||||
|
||||
export async function MyComponent() {
|
||||
const headersList = await headers()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error #5: Parallel route missing `default.js`
|
||||
|
||||
**Error Message**:
|
||||
```
|
||||
Error: Parallel route @modal/login was matched but no default.js was found
|
||||
```
|
||||
|
||||
**Cause**: Next.js 16 requires `default.js` for all parallel routes.
|
||||
|
||||
**Solution**:
|
||||
```typescript
|
||||
// Create app/@modal/default.tsx
|
||||
export default function ModalDefault() {
|
||||
return null
|
||||
}
|
||||
```
|
||||
|
||||
**Structure**:
|
||||
```
|
||||
app/
|
||||
├── @modal/
|
||||
│ ├── login/
|
||||
│ │ └── page.tsx
|
||||
│ └── default.tsx ← REQUIRED
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error #6: `revalidateTag()` requires 2 arguments
|
||||
|
||||
**Error Message**:
|
||||
```
|
||||
Expected 2 arguments, but got 1
|
||||
```
|
||||
|
||||
**Cause**: `revalidateTag()` API changed in Next.js 16.
|
||||
|
||||
**Solution**:
|
||||
```typescript
|
||||
// ❌ Before (Next.js 15)
|
||||
import { revalidateTag } from 'next/cache'
|
||||
revalidateTag('posts')
|
||||
|
||||
// ✅ After (Next.js 16)
|
||||
import { revalidateTag } from 'next/cache'
|
||||
revalidateTag('posts', 'max') // Second argument required
|
||||
```
|
||||
|
||||
**Cache Life Profiles**:
|
||||
- `'max'` - Maximum staleness (recommended)
|
||||
- `'hours'` - Stale after hours
|
||||
- `'days'` - Stale after days
|
||||
- Custom: `{ stale: 3600, revalidate: 86400 }`
|
||||
|
||||
---
|
||||
|
||||
## Error #7: Cannot use React hooks in Server Component
|
||||
|
||||
**Error Message**:
|
||||
```
|
||||
You're importing a component that needs useState. It only works in a Client Component
|
||||
```
|
||||
|
||||
**Cause**: Using React hooks in Server Component.
|
||||
|
||||
**Solution**: Add `'use client'` directive:
|
||||
```typescript
|
||||
// ✅ Add 'use client' at the top
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
|
||||
export function Counter() {
|
||||
const [count, setCount] = useState(0)
|
||||
return <button onClick={() => setCount(count + 1)}>{count}</button>
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error #8: `middleware.ts` is deprecated
|
||||
|
||||
**Warning Message**:
|
||||
```
|
||||
Warning: middleware.ts is deprecated. Use proxy.ts instead.
|
||||
```
|
||||
|
||||
**Solution**: Migrate to `proxy.ts`:
|
||||
```bash
|
||||
# 1. Rename file
|
||||
mv middleware.ts proxy.ts
|
||||
|
||||
# 2. Rename function
|
||||
# middleware → proxy
|
||||
```
|
||||
|
||||
**Code**:
|
||||
```typescript
|
||||
// ✅ proxy.ts
|
||||
export function proxy(request: NextRequest) {
|
||||
// Same logic
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error #9: Turbopack build failure
|
||||
|
||||
**Error Message**:
|
||||
```
|
||||
Error: Failed to compile with Turbopack
|
||||
```
|
||||
|
||||
**Cause**: Turbopack is now default in Next.js 16.
|
||||
|
||||
**Solution 1** (opt-out):
|
||||
```bash
|
||||
npm run build -- --webpack
|
||||
```
|
||||
|
||||
**Solution 2** (fix compatibility):
|
||||
Check for incompatible packages and update them.
|
||||
|
||||
---
|
||||
|
||||
## Error #10: Invalid `next/image` src
|
||||
|
||||
**Error Message**:
|
||||
```
|
||||
Invalid src prop (https://example.com/image.jpg) on `next/image`. Hostname "example.com" is not configured
|
||||
```
|
||||
|
||||
**Cause**: Remote images not configured.
|
||||
|
||||
**Solution**: Add to `next.config.ts`:
|
||||
```typescript
|
||||
const config = {
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'example.com',
|
||||
pathname: '/images/**',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error #11: Cannot import Server Component into Client Component
|
||||
|
||||
**Error Message**:
|
||||
```
|
||||
You're importing a Server Component into a Client Component
|
||||
```
|
||||
|
||||
**Cause**: Direct import of Server Component in Client Component.
|
||||
|
||||
**Solution**: Pass as children:
|
||||
```typescript
|
||||
// ❌ Wrong
|
||||
'use client'
|
||||
import { ServerComponent } from './server-component'
|
||||
|
||||
export function ClientComponent() {
|
||||
return <ServerComponent />
|
||||
}
|
||||
|
||||
// ✅ Correct
|
||||
'use client'
|
||||
|
||||
export function ClientComponent({ children }: { children: React.ReactNode }) {
|
||||
return <div>{children}</div>
|
||||
}
|
||||
|
||||
// Usage
|
||||
<ClientComponent>
|
||||
<ServerComponent /> {/* Pass as children */}
|
||||
</ClientComponent>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error #12: `generateStaticParams` not working
|
||||
|
||||
**Error Message**:
|
||||
```
|
||||
generateStaticParams is not generating static pages
|
||||
```
|
||||
|
||||
**Cause**: Missing `dynamic = 'force-static'`.
|
||||
|
||||
**Solution**:
|
||||
```typescript
|
||||
export const dynamic = 'force-static'
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const posts = await fetch('/api/posts').then(r => r.json())
|
||||
return posts.map((post: { id: string }) => ({ id: post.id }))
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error #13: `fetch()` not caching
|
||||
|
||||
**Error Message**: Data not cached (performance issue).
|
||||
|
||||
**Cause**: Next.js 16 uses opt-in caching.
|
||||
|
||||
**Solution**: Add `"use cache"`:
|
||||
```typescript
|
||||
'use cache'
|
||||
|
||||
export async function getPosts() {
|
||||
const response = await fetch('/api/posts')
|
||||
return response.json()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error #14: Route collision with Route Groups
|
||||
|
||||
**Error Message**:
|
||||
```
|
||||
Error: Conflicting routes: /about and /(marketing)/about
|
||||
```
|
||||
|
||||
**Cause**: Route groups creating same URL path.
|
||||
|
||||
**Solution**: Ensure unique paths:
|
||||
```
|
||||
app/
|
||||
├── (marketing)/about/page.tsx → /about
|
||||
└── (shop)/store-info/page.tsx → /store-info (NOT /about)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error #15: Metadata not updating
|
||||
|
||||
**Error Message**: SEO metadata not showing correctly.
|
||||
|
||||
**Cause**: Using static metadata for dynamic pages.
|
||||
|
||||
**Solution**: Use `generateMetadata()`:
|
||||
```typescript
|
||||
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
|
||||
const { id } = await params
|
||||
const post = await fetch(`/api/posts/${id}`).then(r => r.json())
|
||||
|
||||
return {
|
||||
title: post.title,
|
||||
description: post.excerpt,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error #16: `next/font` font not loading
|
||||
|
||||
**Error Message**: Custom fonts not applying.
|
||||
|
||||
**Cause**: Font variable not applied to HTML element.
|
||||
|
||||
**Solution**:
|
||||
```typescript
|
||||
import { Inter } from 'next/font/google'
|
||||
|
||||
const inter = Inter({ subsets: ['latin'], variable: '--font-inter' })
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html className={inter.variable}> {/* ✅ Apply variable */}
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error #17: Environment variables not available in browser
|
||||
|
||||
**Error Message**: `process.env.SECRET_KEY` is undefined in client.
|
||||
|
||||
**Cause**: Server-only env vars not exposed to browser.
|
||||
|
||||
**Solution**: Prefix with `NEXT_PUBLIC_`:
|
||||
```bash
|
||||
# .env
|
||||
SECRET_KEY=abc123 # Server-only
|
||||
NEXT_PUBLIC_API_URL=https://api # Available in browser
|
||||
```
|
||||
|
||||
```typescript
|
||||
// Server Component (both work)
|
||||
const secret = process.env.SECRET_KEY
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL
|
||||
|
||||
// Client Component (only public vars)
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error #18: Server Action not found
|
||||
|
||||
**Error Message**:
|
||||
```
|
||||
Error: Could not find Server Action
|
||||
```
|
||||
|
||||
**Cause**: Missing `'use server'` directive.
|
||||
|
||||
**Solution**:
|
||||
```typescript
|
||||
// ❌ Before
|
||||
export async function createPost(formData: FormData) {
|
||||
await db.posts.create({ ... })
|
||||
}
|
||||
|
||||
// ✅ After
|
||||
'use server'
|
||||
|
||||
export async function createPost(formData: FormData) {
|
||||
await db.posts.create({ ... })
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Error Lookup
|
||||
|
||||
| Error Type | Solution | Link |
|
||||
|------------|----------|------|
|
||||
| Async params | Add `await params` | [#1](#error-1-params-is-a-promise) |
|
||||
| Async searchParams | Add `await searchParams` | [#2](#error-2-searchparams-is-a-promise) |
|
||||
| Async cookies() | Add `await cookies()` | [#3](#error-3-cookies-requires-await) |
|
||||
| Async headers() | Add `await headers()` | [#4](#error-4-headers-requires-await) |
|
||||
| Missing default.js | Create `default.tsx` | [#5](#error-5-parallel-route-missing-defaultjs) |
|
||||
| revalidateTag 1 arg | Add `cacheLife` argument | [#6](#error-6-revalidatetag-requires-2-arguments) |
|
||||
| Hooks in Server Component | Add `'use client'` | [#7](#error-7-cannot-use-react-hooks-in-server-component) |
|
||||
| middleware.ts deprecated | Rename to `proxy.ts` | [#8](#error-8-middlewarets-is-deprecated) |
|
||||
| Turbopack failure | Use `--webpack` flag | [#9](#error-9-turbopack-build-failure) |
|
||||
| Invalid image src | Add `remotePatterns` | [#10](#error-10-invalid-nextimage-src) |
|
||||
| Import Server in Client | Pass as children | [#11](#error-11-cannot-import-server-component-into-client-component) |
|
||||
| generateStaticParams | Add `dynamic = 'force-static'` | [#12](#error-12-generatestaticparams-not-working) |
|
||||
| fetch not caching | Add `'use cache'` | [#13](#error-13-fetch-not-caching) |
|
||||
| Route collision | Use unique paths | [#14](#error-14-route-collision-with-route-groups) |
|
||||
| Metadata not updating | Use `generateMetadata()` | [#15](#error-15-metadata-not-updating) |
|
||||
| Font not loading | Apply font variable to `<html>` | [#16](#error-16-nextfont-font-not-loading) |
|
||||
| Env vars in browser | Prefix with `NEXT_PUBLIC_` | [#17](#error-17-environment-variables-not-available-in-browser) |
|
||||
| Server Action not found | Add `'use server'` | [#18](#error-18-server-action-not-found) |
|
||||
|
||||
---
|
||||
|
||||
## Prevention Checklist
|
||||
|
||||
Before deploying, check:
|
||||
|
||||
- [ ] All `params` are awaited
|
||||
- [ ] All `searchParams` are awaited
|
||||
- [ ] All `cookies()` calls are awaited
|
||||
- [ ] All `headers()` calls are awaited
|
||||
- [ ] All parallel routes have `default.js`
|
||||
- [ ] `revalidateTag()` has 2 arguments
|
||||
- [ ] Client Components have `'use client'`
|
||||
- [ ] `middleware.ts` migrated to `proxy.ts`
|
||||
- [ ] Remote images configured in `next.config.ts`
|
||||
- [ ] Server Components not imported directly in Client Components
|
||||
- [ ] Static pages have `dynamic = 'force-static'`
|
||||
- [ ] Cached components have `'use cache'`
|
||||
- [ ] No route collisions with Route Groups
|
||||
- [ ] Dynamic pages use `generateMetadata()`
|
||||
- [ ] Fonts applied to `<html>` or `<body>`
|
||||
- [ ] Public env vars prefixed with `NEXT_PUBLIC_`
|
||||
- [ ] Server Actions have `'use server'`
|
||||
- [ ] Node.js version is 20.9+
|
||||
|
||||
---
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
### Enable TypeScript Strict Mode
|
||||
|
||||
```json
|
||||
// tsconfig.json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Check Build Output
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Look for warnings and errors in build logs.
|
||||
|
||||
### Use Type Checking
|
||||
|
||||
```bash
|
||||
npx tsc --noEmit
|
||||
```
|
||||
|
||||
### Check Runtime Logs
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Watch console for errors and warnings.
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
- **Migration Guide**: `references/next-16-migration-guide.md`
|
||||
- **Templates**: `templates/` directory
|
||||
- **Next.js 16 Blog**: https://nextjs.org/blog/next-16
|
||||
- **Support**: jeremy@jezweb.net
|
||||
207
scripts/check-versions.sh
Executable file
207
scripts/check-versions.sh
Executable file
@@ -0,0 +1,207 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Next.js 16 - Version Checker
|
||||
# Verifies that all dependencies are compatible with Next.js 16
|
||||
|
||||
set -e
|
||||
|
||||
echo "🔍 Checking Next.js 16 compatibility..."
|
||||
echo ""
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Check if package.json exists
|
||||
if [ ! -f "package.json" ]; then
|
||||
echo -e "${RED}❌ package.json not found${NC}"
|
||||
echo "Run this script from your project root directory."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check Node.js version
|
||||
echo "📦 Node.js Version:"
|
||||
NODE_VERSION=$(node --version | cut -d'v' -f2)
|
||||
NODE_MAJOR=$(echo $NODE_VERSION | cut -d'.' -f1)
|
||||
NODE_MINOR=$(echo $NODE_VERSION | cut -d'.' -f2)
|
||||
|
||||
if [ "$NODE_MAJOR" -lt 20 ]; then
|
||||
echo -e "${RED}❌ Node.js $NODE_VERSION (requires 20.9+)${NC}"
|
||||
echo " Upgrade: nvm install 20 && nvm use 20"
|
||||
exit 1
|
||||
elif [ "$NODE_MAJOR" -eq 20 ] && [ "$NODE_MINOR" -lt 9 ]; then
|
||||
echo -e "${RED}❌ Node.js $NODE_VERSION (requires 20.9+)${NC}"
|
||||
echo " Upgrade: nvm install 20 && nvm use 20"
|
||||
exit 1
|
||||
else
|
||||
echo -e "${GREEN}✅ Node.js $NODE_VERSION${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Check Next.js version
|
||||
echo "🔧 Next.js Version:"
|
||||
if [ -f "node_modules/next/package.json" ]; then
|
||||
NEXT_VERSION=$(node -p "require('./node_modules/next/package.json').version")
|
||||
NEXT_MAJOR=$(echo $NEXT_VERSION | cut -d'.' -f1)
|
||||
|
||||
if [ "$NEXT_MAJOR" -lt 16 ]; then
|
||||
echo -e "${RED}❌ Next.js $NEXT_VERSION (requires 16.0.0+)${NC}"
|
||||
echo " Upgrade: npm install next@16"
|
||||
exit 1
|
||||
else
|
||||
echo -e "${GREEN}✅ Next.js $NEXT_VERSION${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Next.js not installed (run npm install)${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Check React version
|
||||
echo "⚛️ React Version:"
|
||||
if [ -f "node_modules/react/package.json" ]; then
|
||||
REACT_VERSION=$(node -p "require('./node_modules/react/package.json').version")
|
||||
REACT_MAJOR=$(echo $REACT_VERSION | cut -d'.' -f1)
|
||||
REACT_MINOR=$(echo $REACT_VERSION | cut -d'.' -f2)
|
||||
|
||||
if [ "$REACT_MAJOR" -lt 19 ]; then
|
||||
echo -e "${RED}❌ React $REACT_VERSION (requires 19.2+)${NC}"
|
||||
echo " Upgrade: npm install react@19.2 react-dom@19.2"
|
||||
exit 1
|
||||
elif [ "$REACT_MAJOR" -eq 19 ] && [ "$REACT_MINOR" -lt 2 ]; then
|
||||
echo -e "${YELLOW}⚠️ React $REACT_VERSION (recommends 19.2+)${NC}"
|
||||
echo " Upgrade: npm install react@19.2 react-dom@19.2"
|
||||
else
|
||||
echo -e "${GREEN}✅ React $REACT_VERSION${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ React not installed (run npm install)${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Check TypeScript version (if using TypeScript)
|
||||
if [ -f "tsconfig.json" ]; then
|
||||
echo "📘 TypeScript Version:"
|
||||
if [ -f "node_modules/typescript/package.json" ]; then
|
||||
TS_VERSION=$(node -p "require('./node_modules/typescript/package.json').version")
|
||||
TS_MAJOR=$(echo $TS_VERSION | cut -d'.' -f1)
|
||||
TS_MINOR=$(echo $TS_VERSION | cut -d'.' -f2)
|
||||
|
||||
if [ "$TS_MAJOR" -lt 5 ]; then
|
||||
echo -e "${RED}❌ TypeScript $TS_VERSION (requires 5.1+)${NC}"
|
||||
echo " Upgrade: npm install -D typescript@latest"
|
||||
exit 1
|
||||
elif [ "$TS_MAJOR" -eq 5 ] && [ "$TS_MINOR" -lt 1 ]; then
|
||||
echo -e "${RED}❌ TypeScript $TS_VERSION (requires 5.1+)${NC}"
|
||||
echo " Upgrade: npm install -D typescript@latest"
|
||||
exit 1
|
||||
else
|
||||
echo -e "${GREEN}✅ TypeScript $TS_VERSION${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ TypeScript not installed (run npm install)${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Check for deprecated files
|
||||
echo "🔎 Checking for deprecated patterns..."
|
||||
|
||||
DEPRECATED_FOUND=0
|
||||
|
||||
if [ -f "middleware.ts" ]; then
|
||||
echo -e "${YELLOW}⚠️ middleware.ts found (deprecated in Next.js 16)${NC}"
|
||||
echo " Migrate: Rename to proxy.ts and update function name"
|
||||
DEPRECATED_FOUND=1
|
||||
fi
|
||||
|
||||
if [ -f "middleware.js" ]; then
|
||||
echo -e "${YELLOW}⚠️ middleware.js found (deprecated in Next.js 16)${NC}"
|
||||
echo " Migrate: Rename to proxy.js and update function name"
|
||||
DEPRECATED_FOUND=1
|
||||
fi
|
||||
|
||||
# Check for parallel routes missing default.js
|
||||
if [ -d "app" ]; then
|
||||
PARALLEL_ROUTES=$(find app -type d -name '@*' 2>/dev/null)
|
||||
|
||||
if [ ! -z "$PARALLEL_ROUTES" ]; then
|
||||
for route in $PARALLEL_ROUTES; do
|
||||
if [ ! -f "$route/default.tsx" ] && [ ! -f "$route/default.jsx" ] && [ ! -f "$route/default.js" ]; then
|
||||
echo -e "${YELLOW}⚠️ $route missing default.tsx (required in Next.js 16)${NC}"
|
||||
echo " Create: touch $route/default.tsx"
|
||||
DEPRECATED_FOUND=1
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ $DEPRECATED_FOUND -eq 0 ]; then
|
||||
echo -e "${GREEN}✅ No deprecated patterns found${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Summary
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "📊 Summary:"
|
||||
echo ""
|
||||
|
||||
ALL_GOOD=1
|
||||
|
||||
# Node.js check
|
||||
if [ "$NODE_MAJOR" -ge 20 ] && [ "$NODE_MINOR" -ge 9 ]; then
|
||||
echo -e "${GREEN}✅ Node.js compatible${NC}"
|
||||
else
|
||||
echo -e "${RED}❌ Node.js incompatible${NC}"
|
||||
ALL_GOOD=0
|
||||
fi
|
||||
|
||||
# Next.js check
|
||||
if [ -f "node_modules/next/package.json" ]; then
|
||||
if [ "$NEXT_MAJOR" -ge 16 ]; then
|
||||
echo -e "${GREEN}✅ Next.js compatible${NC}"
|
||||
else
|
||||
echo -e "${RED}❌ Next.js incompatible${NC}"
|
||||
ALL_GOOD=0
|
||||
fi
|
||||
fi
|
||||
|
||||
# React check
|
||||
if [ -f "node_modules/react/package.json" ]; then
|
||||
if [ "$REACT_MAJOR" -ge 19 ]; then
|
||||
echo -e "${GREEN}✅ React compatible${NC}"
|
||||
else
|
||||
echo -e "${RED}❌ React incompatible${NC}"
|
||||
ALL_GOOD=0
|
||||
fi
|
||||
fi
|
||||
|
||||
# TypeScript check (if applicable)
|
||||
if [ -f "tsconfig.json" ] && [ -f "node_modules/typescript/package.json" ]; then
|
||||
if [ "$TS_MAJOR" -ge 5 ] && [ "$TS_MINOR" -ge 1 ]; then
|
||||
echo -e "${GREEN}✅ TypeScript compatible${NC}"
|
||||
else
|
||||
echo -e "${RED}❌ TypeScript incompatible${NC}"
|
||||
ALL_GOOD=0
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
if [ $ALL_GOOD -eq 1 ] && [ $DEPRECATED_FOUND -eq 0 ]; then
|
||||
echo -e "${GREEN}🎉 All checks passed! Your project is ready for Next.js 16.${NC}"
|
||||
exit 0
|
||||
elif [ $ALL_GOOD -eq 1 ]; then
|
||||
echo -e "${YELLOW}⚠️ Dependencies compatible, but deprecated patterns found.${NC}"
|
||||
echo "Fix deprecation warnings before migrating to Next.js 16."
|
||||
exit 1
|
||||
else
|
||||
echo -e "${RED}❌ Compatibility issues found. Fix errors above before continuing.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
282
templates/app-router-async-params.tsx
Normal file
282
templates/app-router-async-params.tsx
Normal file
@@ -0,0 +1,282 @@
|
||||
/**
|
||||
* Next.js 16 - Async Route Parameters
|
||||
*
|
||||
* BREAKING CHANGE: params, searchParams, cookies(), headers(), draftMode()
|
||||
* are now async and must be awaited in Next.js 16.
|
||||
*
|
||||
* This template shows the correct patterns for accessing route parameters,
|
||||
* search parameters, cookies, and headers in Next.js 16.
|
||||
*/
|
||||
|
||||
import { cookies, headers, draftMode } from 'next/headers'
|
||||
import { notFound } from 'next/navigation'
|
||||
|
||||
// ============================================================================
|
||||
// Example 1: Page with Async Params
|
||||
// ============================================================================
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ slug: string }>
|
||||
searchParams: Promise<{ q?: string; page?: string }>
|
||||
}
|
||||
|
||||
export default async function BlogPostPage({ params, searchParams }: PageProps) {
|
||||
// ✅ Await params and searchParams in Next.js 16
|
||||
const { slug } = await params
|
||||
const { q, page } = await searchParams
|
||||
|
||||
// Fetch post data
|
||||
const post = await fetch(`https://api.example.com/posts/${slug}`)
|
||||
.then(r => r.json())
|
||||
.catch(() => null)
|
||||
|
||||
if (!post) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
return (
|
||||
<article>
|
||||
<h1>{post.title}</h1>
|
||||
<div dangerouslySetInnerHTML={{ __html: post.content }} />
|
||||
|
||||
{/* Show search query if present */}
|
||||
{q && <p>Search query: {q}</p>}
|
||||
|
||||
{/* Show page number if present */}
|
||||
{page && <p>Page: {page}</p>}
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 2: Layout with Async Params
|
||||
// ============================================================================
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode
|
||||
params: Promise<{ category: string }>
|
||||
}
|
||||
|
||||
export async function ProductLayout({ children, params }: LayoutProps) {
|
||||
// ✅ Await params in layouts too
|
||||
const { category } = await params
|
||||
|
||||
return (
|
||||
<div>
|
||||
<nav>
|
||||
<h2>Category: {category}</h2>
|
||||
</nav>
|
||||
<main>{children}</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 3: Accessing Cookies (Async in Next.js 16)
|
||||
// ============================================================================
|
||||
|
||||
export async function UserGreeting() {
|
||||
// ✅ Await cookies() in Next.js 16
|
||||
const cookieStore = await cookies()
|
||||
const userId = cookieStore.get('userId')?.value
|
||||
const theme = cookieStore.get('theme')?.value || 'light'
|
||||
|
||||
if (!userId) {
|
||||
return <p>Welcome, Guest!</p>
|
||||
}
|
||||
|
||||
const user = await fetch(`https://api.example.com/users/${userId}`)
|
||||
.then(r => r.json())
|
||||
|
||||
return (
|
||||
<div data-theme={theme}>
|
||||
<p>Welcome back, {user.name}!</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 4: Accessing Headers (Async in Next.js 16)
|
||||
// ============================================================================
|
||||
|
||||
export async function RequestInfo() {
|
||||
// ✅ Await headers() in Next.js 16
|
||||
const headersList = await headers()
|
||||
const userAgent = headersList.get('user-agent') || 'Unknown'
|
||||
const referer = headersList.get('referer') || 'Direct'
|
||||
const ip = headersList.get('x-forwarded-for') || 'Unknown'
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>User Agent: {userAgent}</p>
|
||||
<p>Referrer: {referer}</p>
|
||||
<p>IP: {ip}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 5: Draft Mode (Async in Next.js 16)
|
||||
// ============================================================================
|
||||
|
||||
export async function DraftBanner() {
|
||||
// ✅ Await draftMode() in Next.js 16
|
||||
const { isEnabled } = await draftMode()
|
||||
|
||||
if (!isEnabled) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ background: 'yellow', padding: '1rem' }}>
|
||||
<p>🚧 Draft Mode Enabled</p>
|
||||
<a href="/api/disable-draft">Exit Draft Mode</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 6: Generate Metadata with Async Params
|
||||
// ============================================================================
|
||||
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
||||
// ✅ Await params in generateMetadata
|
||||
const { slug } = await params
|
||||
|
||||
const post = await fetch(`https://api.example.com/posts/${slug}`)
|
||||
.then(r => r.json())
|
||||
|
||||
return {
|
||||
title: post.title,
|
||||
description: post.excerpt,
|
||||
openGraph: {
|
||||
title: post.title,
|
||||
description: post.excerpt,
|
||||
images: [post.coverImage],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 7: Generate Static Params (Async)
|
||||
// ============================================================================
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const posts = await fetch('https://api.example.com/posts')
|
||||
.then(r => r.json())
|
||||
|
||||
return posts.map((post: { slug: string }) => ({
|
||||
slug: post.slug,
|
||||
}))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 8: Route Handler with Async Params
|
||||
// ============================================================================
|
||||
|
||||
// File: app/api/posts/[id]/route.ts
|
||||
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
// ✅ Await params in route handlers
|
||||
const { id } = await params
|
||||
|
||||
const post = await fetch(`https://api.example.com/posts/${id}`)
|
||||
.then(r => r.json())
|
||||
|
||||
return NextResponse.json(post)
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
// ✅ Await params in route handlers
|
||||
const { id } = await params
|
||||
|
||||
await fetch(`https://api.example.com/posts/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
return NextResponse.json({ message: 'Post deleted' })
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Migration Guide: Next.js 15 → Next.js 16
|
||||
// ============================================================================
|
||||
|
||||
// ❌ BEFORE (Next.js 15):
|
||||
/*
|
||||
export default function Page({ params, searchParams }) {
|
||||
const slug = params.slug // ❌ Sync access
|
||||
const query = searchParams.q // ❌ Sync access
|
||||
}
|
||||
|
||||
export function MyComponent() {
|
||||
const cookieStore = cookies() // ❌ Sync access
|
||||
const headersList = headers() // ❌ Sync access
|
||||
}
|
||||
*/
|
||||
|
||||
// ✅ AFTER (Next.js 16):
|
||||
/*
|
||||
export default async function Page({ params, searchParams }) {
|
||||
const { slug } = await params // ✅ Async access
|
||||
const { q: query } = await searchParams // ✅ Async access
|
||||
}
|
||||
|
||||
export async function MyComponent() {
|
||||
const cookieStore = await cookies() // ✅ Async access
|
||||
const headersList = await headers() // ✅ Async access
|
||||
}
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// TypeScript Types
|
||||
// ============================================================================
|
||||
|
||||
// Correct types for Next.js 16:
|
||||
type Params<T = Record<string, string>> = Promise<T>
|
||||
type SearchParams = Promise<{ [key: string]: string | string[] | undefined }>
|
||||
|
||||
// Usage:
|
||||
type ProductPageProps = {
|
||||
params: Params<{ id: string }>
|
||||
searchParams: SearchParams
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Codemod (Automatic Migration)
|
||||
// ============================================================================
|
||||
|
||||
// Run this command to automatically migrate your code:
|
||||
// npx @next/codemod@canary upgrade latest
|
||||
|
||||
/**
|
||||
* Summary:
|
||||
*
|
||||
* 1. ALL route parameters are now async:
|
||||
* - params → await params
|
||||
* - searchParams → await searchParams
|
||||
*
|
||||
* 2. ALL next/headers functions are now async:
|
||||
* - cookies() → await cookies()
|
||||
* - headers() → await headers()
|
||||
* - draftMode() → await draftMode()
|
||||
*
|
||||
* 3. Components using these must be async:
|
||||
* - export default async function Page({ params }) { ... }
|
||||
* - export async function Layout({ params }) { ... }
|
||||
* - export async function generateMetadata({ params }) { ... }
|
||||
*
|
||||
* 4. Route handlers must await params:
|
||||
* - export async function GET(request, { params }) {
|
||||
* const { id } = await params
|
||||
* }
|
||||
*/
|
||||
343
templates/cache-component-use-cache.tsx
Normal file
343
templates/cache-component-use-cache.tsx
Normal file
@@ -0,0 +1,343 @@
|
||||
/**
|
||||
* Next.js 16 - Cache Components with "use cache" Directive
|
||||
*
|
||||
* NEW in Next.js 16: Explicit opt-in caching with "use cache" directive.
|
||||
* Replaces implicit caching from Next.js 15.
|
||||
*
|
||||
* This template shows component-level, function-level, and page-level caching.
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Example 1: Component-Level Caching
|
||||
// ============================================================================
|
||||
|
||||
'use cache'
|
||||
|
||||
// This entire component will be cached
|
||||
export async function CachedProductList() {
|
||||
const products = await fetch('https://api.example.com/products')
|
||||
.then(r => r.json())
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Products</h2>
|
||||
<ul>
|
||||
{products.map((product: { id: string; name: string; price: number }) => (
|
||||
<li key={product.id}>
|
||||
{product.name} - ${product.price}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 2: Function-Level Caching
|
||||
// ============================================================================
|
||||
|
||||
// File: lib/data.ts
|
||||
'use cache'
|
||||
|
||||
export async function getExpensiveData(id: string) {
|
||||
console.log('Fetching expensive data...') // Only logs on cache miss
|
||||
|
||||
// Simulate expensive operation
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
const data = await fetch(`https://api.example.com/items/${id}`)
|
||||
.then(r => r.json())
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// Usage in component (not cached itself):
|
||||
import { getExpensiveData } from '@/lib/data'
|
||||
|
||||
export async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params
|
||||
const product = await getExpensiveData(id) // Cached by function
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{product.name}</h1>
|
||||
<p>{product.description}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 3: Page-Level Caching
|
||||
// ============================================================================
|
||||
|
||||
// File: app/blog/[slug]/page.tsx
|
||||
'use cache'
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const posts = await fetch('https://api.example.com/posts')
|
||||
.then(r => r.json())
|
||||
|
||||
return posts.map((post: { slug: string }) => ({
|
||||
slug: post.slug,
|
||||
}))
|
||||
}
|
||||
|
||||
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
|
||||
const { slug } = await params
|
||||
|
||||
const post = await fetch(`https://api.example.com/posts/${slug}`)
|
||||
.then(r => r.json())
|
||||
|
||||
return (
|
||||
<article>
|
||||
<h1>{post.title}</h1>
|
||||
<div dangerouslySetInnerHTML={{ __html: post.content }} />
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 4: Partial Prerendering (PPR) - Mix Static & Dynamic
|
||||
// ============================================================================
|
||||
|
||||
// File: app/dashboard/page.tsx
|
||||
|
||||
// Static component (cached)
|
||||
'use cache'
|
||||
async function StaticHeader() {
|
||||
return (
|
||||
<header>
|
||||
<h1>My Dashboard</h1>
|
||||
<nav>
|
||||
<a href="/dashboard">Overview</a>
|
||||
<a href="/dashboard/settings">Settings</a>
|
||||
</nav>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
// Dynamic component (NOT cached) - separate file without "use cache"
|
||||
// File: components/dynamic-user-info.tsx
|
||||
import { cookies } from 'next/headers'
|
||||
|
||||
export async function DynamicUserInfo() {
|
||||
const cookieStore = await cookies()
|
||||
const userId = cookieStore.get('userId')?.value
|
||||
|
||||
if (!userId) {
|
||||
return <div>Please log in</div>
|
||||
}
|
||||
|
||||
const user = await fetch(`https://api.example.com/users/${userId}`)
|
||||
.then(r => r.json())
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Welcome, {user.name}</p>
|
||||
<p>Balance: ${user.balance}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Page combines static + dynamic (Partial Prerendering)
|
||||
import { DynamicUserInfo } from '@/components/dynamic-user-info'
|
||||
|
||||
export default function DashboardPage() {
|
||||
return (
|
||||
<div>
|
||||
<StaticHeader /> {/* Cached (static) */}
|
||||
<DynamicUserInfo /> {/* Not cached (dynamic) */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 5: Selective Caching with Multiple Functions
|
||||
// ============================================================================
|
||||
|
||||
// Cache expensive operations, skip cheap ones
|
||||
|
||||
// Cached function
|
||||
'use cache'
|
||||
export async function getPopularPosts() {
|
||||
const posts = await fetch('https://api.example.com/posts/popular')
|
||||
.then(r => r.json())
|
||||
|
||||
return posts
|
||||
}
|
||||
|
||||
// NOT cached (changes frequently)
|
||||
export async function getRealtimeMetrics() {
|
||||
const metrics = await fetch('https://api.example.com/metrics/realtime')
|
||||
.then(r => r.json())
|
||||
|
||||
return metrics
|
||||
}
|
||||
|
||||
// Component uses both
|
||||
export async function Dashboard() {
|
||||
const popularPosts = await getPopularPosts() // Cached
|
||||
const metrics = await getRealtimeMetrics() // NOT cached
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<h2>Popular Posts</h2>
|
||||
<ul>
|
||||
{popularPosts.map((post: { id: string; title: string }) => (
|
||||
<li key={post.id}>{post.title}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Realtime Metrics</h2>
|
||||
<p>Active users: {metrics.activeUsers}</p>
|
||||
<p>Requests/min: {metrics.requestsPerMinute}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 6: Cache with Revalidation (using tags)
|
||||
// ============================================================================
|
||||
|
||||
// File: app/actions.ts
|
||||
'use server'
|
||||
|
||||
import { revalidateTag } from 'next/cache'
|
||||
|
||||
export async function createPost(formData: FormData) {
|
||||
const title = formData.get('title') as string
|
||||
const content = formData.get('content') as string
|
||||
|
||||
await fetch('https://api.example.com/posts', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ title, content }),
|
||||
})
|
||||
|
||||
// Revalidate cached posts
|
||||
revalidateTag('posts', 'max')
|
||||
}
|
||||
|
||||
// File: lib/posts.ts
|
||||
'use cache'
|
||||
|
||||
export async function getPosts() {
|
||||
const response = await fetch('https://api.example.com/posts', {
|
||||
next: { tags: ['posts'] }, // Tag for revalidation
|
||||
})
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 7: Conditional Caching (Cache Based on User Role)
|
||||
// ============================================================================
|
||||
|
||||
import { cookies } from 'next/headers'
|
||||
|
||||
export async function getContent() {
|
||||
const cookieStore = await cookies()
|
||||
const userRole = cookieStore.get('role')?.value
|
||||
|
||||
if (userRole === 'admin') {
|
||||
// Don't cache admin content (changes frequently)
|
||||
return fetch('https://api.example.com/admin/content').then(r => r.json())
|
||||
}
|
||||
|
||||
// Cache public content
|
||||
return getCachedPublicContent()
|
||||
}
|
||||
|
||||
'use cache'
|
||||
async function getCachedPublicContent() {
|
||||
return fetch('https://api.example.com/public/content').then(r => r.json())
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 8: Inline "use cache" (Granular Control)
|
||||
// ============================================================================
|
||||
|
||||
export async function MixedCachingComponent() {
|
||||
// This function call is cached
|
||||
const cachedData = await (async function() {
|
||||
'use cache'
|
||||
return fetch('https://api.example.com/slow-data').then(r => r.json())
|
||||
})()
|
||||
|
||||
// This function call is NOT cached
|
||||
const freshData = await fetch('https://api.example.com/fresh-data').then(r => r.json())
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>Cached: {cachedData.value}</div>
|
||||
<div>Fresh: {freshData.value}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Migration Guide: Next.js 15 → Next.js 16
|
||||
// ============================================================================
|
||||
|
||||
// ❌ BEFORE (Next.js 15 - Implicit Caching):
|
||||
/*
|
||||
// All Server Components were cached by default
|
||||
export async function MyComponent() {
|
||||
const data = await fetch('https://api.example.com/data')
|
||||
return <div>{data.value}</div>
|
||||
}
|
||||
|
||||
// To opt-out of caching:
|
||||
export const revalidate = 0 // or export const dynamic = 'force-dynamic'
|
||||
*/
|
||||
|
||||
// ✅ AFTER (Next.js 16 - Explicit Opt-In Caching):
|
||||
/*
|
||||
// Components are NOT cached by default
|
||||
export async function MyComponent() {
|
||||
const data = await fetch('https://api.example.com/data')
|
||||
return <div>{data.value}</div>
|
||||
}
|
||||
|
||||
// To opt-IN to caching, add "use cache"
|
||||
'use cache'
|
||||
export async function MyCachedComponent() {
|
||||
const data = await fetch('https://api.example.com/data')
|
||||
return <div>{data.value}</div>
|
||||
}
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Cache Behavior Summary
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* "use cache" can be added to:
|
||||
* 1. ✅ Components (entire component cached)
|
||||
* 2. ✅ Functions (function output cached)
|
||||
* 3. ✅ Pages (entire page cached)
|
||||
* 4. ✅ Layouts (layout cached)
|
||||
* 5. ✅ Inline async functions (granular caching)
|
||||
*
|
||||
* Default behavior (without "use cache"):
|
||||
* - Server Components: NOT cached (change from Next.js 15)
|
||||
* - fetch() calls: Cached by default (unchanged)
|
||||
*
|
||||
* Revalidation:
|
||||
* - Use revalidateTag() to invalidate cache by tag
|
||||
* - Use updateTag() for immediate read-your-writes
|
||||
* - Use refresh() for uncached data only
|
||||
*
|
||||
* When to use "use cache":
|
||||
* ✅ Expensive computations (database queries, API calls)
|
||||
* ✅ Stable data (product catalogs, blog posts)
|
||||
* ✅ Partial Prerendering (static header + dynamic user info)
|
||||
*
|
||||
* When NOT to use "use cache":
|
||||
* ❌ Real-time data (metrics, notifications)
|
||||
* ❌ User-specific data (unless using cookies/headers for cache keys)
|
||||
* ❌ Frequently changing data (stock prices, live scores)
|
||||
*/
|
||||
32
templates/package.json
Normal file
32
templates/package.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "nextjs-16-app",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "^16.0.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"typescript": "^5.7.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"zod": "^3.24.0",
|
||||
"@tailwindcss/vite": "^4.1.0",
|
||||
"tailwindcss": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.9.0",
|
||||
"npm": ">=10.0.0"
|
||||
}
|
||||
}
|
||||
437
templates/parallel-routes-with-default.tsx
Normal file
437
templates/parallel-routes-with-default.tsx
Normal file
@@ -0,0 +1,437 @@
|
||||
/**
|
||||
* Next.js 16 - Parallel Routes with Required default.js
|
||||
*
|
||||
* BREAKING CHANGE: Parallel routes now REQUIRE explicit default.js files.
|
||||
* Without them, routes will fail during soft navigation.
|
||||
*
|
||||
* Directory structure:
|
||||
* app/
|
||||
* ├── @modal/
|
||||
* │ ├── login/
|
||||
* │ │ └── page.tsx
|
||||
* │ └── default.tsx ← REQUIRED in Next.js 16
|
||||
* ├── @feed/
|
||||
* │ ├── trending/
|
||||
* │ │ └── page.tsx
|
||||
* │ └── default.tsx ← REQUIRED in Next.js 16
|
||||
* └── layout.tsx
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Example 1: Modal + Main Content (Common Pattern)
|
||||
// ============================================================================
|
||||
|
||||
// File: app/layout.tsx
|
||||
export default function RootLayout({
|
||||
children,
|
||||
modal,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
modal: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
{modal}
|
||||
<main>{children}</main>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
||||
// File: app/@modal/login/page.tsx
|
||||
export default function LoginModal() {
|
||||
return (
|
||||
<div className="modal-overlay">
|
||||
<div className="modal">
|
||||
<h2>Login</h2>
|
||||
<form>
|
||||
<input type="email" placeholder="Email" />
|
||||
<input type="password" placeholder="Password" />
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// File: app/@modal/default.tsx (REQUIRED)
|
||||
export default function ModalDefault() {
|
||||
return null // No modal shown by default
|
||||
}
|
||||
|
||||
// File: app/page.tsx
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Home Page</h1>
|
||||
<a href="/login">Open Login Modal</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 2: Dashboard with Multiple Panels
|
||||
// ============================================================================
|
||||
|
||||
// File: app/dashboard/layout.tsx
|
||||
export default function DashboardLayout({
|
||||
children,
|
||||
analytics,
|
||||
notifications,
|
||||
activity,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
analytics: React.ReactNode
|
||||
notifications: React.ReactNode
|
||||
activity: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div className="dashboard-layout">
|
||||
<aside className="sidebar">
|
||||
{notifications}
|
||||
</aside>
|
||||
|
||||
<main className="main-content">
|
||||
{children}
|
||||
{analytics}
|
||||
</main>
|
||||
|
||||
<aside className="activity-sidebar">
|
||||
{activity}
|
||||
</aside>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// File: app/dashboard/@analytics/overview/page.tsx
|
||||
export default async function AnalyticsOverview() {
|
||||
const stats = await fetch('https://api.example.com/stats').then(r => r.json())
|
||||
|
||||
return (
|
||||
<div className="analytics-panel">
|
||||
<h2>Analytics</h2>
|
||||
<div>
|
||||
<p>Page Views: {stats.pageViews}</p>
|
||||
<p>Unique Visitors: {stats.uniqueVisitors}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// File: app/dashboard/@analytics/default.tsx (REQUIRED)
|
||||
export default function AnalyticsDefault() {
|
||||
return (
|
||||
<div className="analytics-panel">
|
||||
<h2>Analytics</h2>
|
||||
<p>No analytics data available</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// File: app/dashboard/@notifications/default.tsx (REQUIRED)
|
||||
export default function NotificationsDefault() {
|
||||
return (
|
||||
<div className="notifications-panel">
|
||||
<h3>Notifications</h3>
|
||||
<p>No new notifications</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// File: app/dashboard/@activity/default.tsx (REQUIRED)
|
||||
export default function ActivityDefault() {
|
||||
return (
|
||||
<div className="activity-panel">
|
||||
<h3>Recent Activity</h3>
|
||||
<p>No recent activity</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 3: E-commerce with Product + Reviews
|
||||
// ============================================================================
|
||||
|
||||
// File: app/products/[id]/layout.tsx
|
||||
export default function ProductLayout({
|
||||
children,
|
||||
reviews,
|
||||
recommendations,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
reviews: React.ReactNode
|
||||
recommendations: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div className="product-layout">
|
||||
<div className="product-main">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
<div className="product-sidebar">
|
||||
{reviews}
|
||||
{recommendations}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// File: app/products/[id]/@reviews/page.tsx
|
||||
export default async function ProductReviews({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params
|
||||
const reviews = await fetch(`https://api.example.com/products/${id}/reviews`)
|
||||
.then(r => r.json())
|
||||
|
||||
return (
|
||||
<div className="reviews">
|
||||
<h3>Reviews</h3>
|
||||
<ul>
|
||||
{reviews.map((review: { id: string; rating: number; comment: string }) => (
|
||||
<li key={review.id}>
|
||||
<p>⭐ {review.rating}/5</p>
|
||||
<p>{review.comment}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// File: app/products/[id]/@reviews/default.tsx (REQUIRED)
|
||||
export default function ReviewsDefault() {
|
||||
return (
|
||||
<div className="reviews">
|
||||
<h3>Reviews</h3>
|
||||
<p>No reviews yet</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// File: app/products/[id]/@recommendations/default.tsx (REQUIRED)
|
||||
export default function RecommendationsDefault() {
|
||||
return (
|
||||
<div className="recommendations">
|
||||
<h3>Recommendations</h3>
|
||||
<p>Loading recommendations...</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 4: Auth-Gated Content
|
||||
// ============================================================================
|
||||
|
||||
import { cookies } from 'next/headers'
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
// File: app/@auth/default.tsx (REQUIRED)
|
||||
export default async function AuthDefault() {
|
||||
const cookieStore = await cookies()
|
||||
const isAuthenticated = cookieStore.get('auth')?.value
|
||||
|
||||
if (!isAuthenticated) {
|
||||
redirect('/login')
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// File: app/@auth/profile/page.tsx
|
||||
export default async function ProfilePage() {
|
||||
const cookieStore = await cookies()
|
||||
const userId = cookieStore.get('userId')?.value
|
||||
|
||||
const user = await fetch(`https://api.example.com/users/${userId}`)
|
||||
.then(r => r.json())
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Profile</h2>
|
||||
<p>Name: {user.name}</p>
|
||||
<p>Email: {user.email}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 5: Conditional Rendering Based on Slot
|
||||
// ============================================================================
|
||||
|
||||
// File: app/layout.tsx
|
||||
export default function Layout({
|
||||
children,
|
||||
banner,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
banner: React.ReactNode
|
||||
}) {
|
||||
// Only show banner on specific pages
|
||||
const showBanner = true // Determine based on route
|
||||
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
{showBanner && banner}
|
||||
<main>{children}</main>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
||||
// File: app/@banner/sale/page.tsx
|
||||
export default function SaleBanner() {
|
||||
return (
|
||||
<div className="banner sale-banner">
|
||||
🎉 50% OFF SALE! Use code SALE50
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// File: app/@banner/default.tsx (REQUIRED)
|
||||
export default function BannerDefault() {
|
||||
return null // No banner by default
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 6: Loading States with Parallel Routes
|
||||
// ============================================================================
|
||||
|
||||
// File: app/dashboard/@analytics/loading.tsx
|
||||
export default function AnalyticsLoading() {
|
||||
return (
|
||||
<div className="analytics-panel">
|
||||
<h2>Analytics</h2>
|
||||
<p>Loading analytics...</p>
|
||||
<div className="skeleton-loader" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// File: app/dashboard/@notifications/loading.tsx
|
||||
export default function NotificationsLoading() {
|
||||
return (
|
||||
<div className="notifications-panel">
|
||||
<h3>Notifications</h3>
|
||||
<div className="skeleton-loader" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 7: Error Boundaries with Parallel Routes
|
||||
// ============================================================================
|
||||
|
||||
// File: app/dashboard/@analytics/error.tsx
|
||||
'use client'
|
||||
|
||||
export default function AnalyticsError({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error
|
||||
reset: () => void
|
||||
}) {
|
||||
return (
|
||||
<div className="analytics-panel error">
|
||||
<h2>Analytics</h2>
|
||||
<p>Failed to load analytics</p>
|
||||
<button onClick={reset}>Try Again</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Migration Guide: Next.js 15 → Next.js 16
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* BREAKING CHANGE: default.js is now REQUIRED for all parallel routes
|
||||
*
|
||||
* ❌ BEFORE (Next.js 15):
|
||||
* app/
|
||||
* ├── @modal/
|
||||
* │ └── login/
|
||||
* │ └── page.tsx
|
||||
* └── layout.tsx
|
||||
*
|
||||
* This worked in Next.js 15. If no matching route, Next.js rendered nothing.
|
||||
*
|
||||
* ✅ AFTER (Next.js 16):
|
||||
* app/
|
||||
* ├── @modal/
|
||||
* │ ├── login/
|
||||
* │ │ └── page.tsx
|
||||
* │ └── default.tsx ← REQUIRED! Will error without this
|
||||
* └── layout.tsx
|
||||
*
|
||||
* Why the change?
|
||||
* Next.js 16 changed how parallel routes handle soft navigation. Without
|
||||
* default.js, unmatched slots will error during client-side navigation.
|
||||
*
|
||||
* What should default.tsx return?
|
||||
* - return null (most common - no UI shown)
|
||||
* - return <Skeleton /> (loading placeholder)
|
||||
* - redirect() to another route
|
||||
* - return fallback UI
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Common Patterns for default.tsx
|
||||
// ============================================================================
|
||||
|
||||
// Pattern 1: Null (no UI)
|
||||
export function DefaultNull() {
|
||||
return null
|
||||
}
|
||||
|
||||
// Pattern 2: Loading skeleton
|
||||
export function DefaultSkeleton() {
|
||||
return (
|
||||
<div className="skeleton">
|
||||
<div className="skeleton-line" />
|
||||
<div className="skeleton-line" />
|
||||
<div className="skeleton-line" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Pattern 3: Fallback message
|
||||
export function DefaultFallback() {
|
||||
return (
|
||||
<div>
|
||||
<p>Content not available</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Pattern 4: Redirect
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export function DefaultRedirect() {
|
||||
redirect('/dashboard')
|
||||
}
|
||||
|
||||
/**
|
||||
* Summary:
|
||||
*
|
||||
* Parallel Routes in Next.js 16:
|
||||
* 1. ✅ Use @folder convention for parallel slots
|
||||
* 2. ✅ MUST include default.tsx for each @folder
|
||||
* 3. ✅ default.tsx handles unmatched routes during navigation
|
||||
* 4. ✅ Can have loading.tsx for loading states
|
||||
* 5. ✅ Can have error.tsx for error boundaries
|
||||
*
|
||||
* Common use cases:
|
||||
* - Modals + main content
|
||||
* - Dashboard panels
|
||||
* - Product + reviews/recommendations
|
||||
* - Conditional banners
|
||||
* - Auth-gated content
|
||||
*
|
||||
* Best practices:
|
||||
* - Keep default.tsx simple (usually return null)
|
||||
* - Use loading.tsx for better UX
|
||||
* - Use error.tsx for error handling
|
||||
* - Test soft navigation (client-side routing)
|
||||
*/
|
||||
274
templates/proxy-migration.ts
Normal file
274
templates/proxy-migration.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
/**
|
||||
* Next.js 16 - Proxy Migration (middleware.ts → proxy.ts)
|
||||
*
|
||||
* BREAKING CHANGE: middleware.ts is deprecated in Next.js 16.
|
||||
* Use proxy.ts instead.
|
||||
*
|
||||
* Migration: Rename file and function, keep same logic.
|
||||
*/
|
||||
|
||||
import { NextResponse } from 'next/server'
|
||||
import type { NextRequest } from 'next/server'
|
||||
|
||||
// ============================================================================
|
||||
// Example 1: Basic Proxy (Auth Check)
|
||||
// ============================================================================
|
||||
|
||||
export function proxy(request: NextRequest) {
|
||||
const token = request.cookies.get('token')
|
||||
|
||||
// Redirect to login if no token
|
||||
if (!token) {
|
||||
return NextResponse.redirect(new URL('/login', request.url))
|
||||
}
|
||||
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: '/dashboard/:path*',
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 2: Advanced Proxy (Multiple Checks)
|
||||
// ============================================================================
|
||||
|
||||
export function advancedProxy(request: NextRequest) {
|
||||
const { pathname } = request.nextUrl
|
||||
|
||||
// 1. Auth check
|
||||
const token = request.cookies.get('token')
|
||||
if (pathname.startsWith('/dashboard') && !token) {
|
||||
return NextResponse.redirect(new URL('/login', request.url))
|
||||
}
|
||||
|
||||
// 2. Role-based access
|
||||
const userRole = request.cookies.get('role')?.value
|
||||
if (pathname.startsWith('/admin') && userRole !== 'admin') {
|
||||
return NextResponse.redirect(new URL('/unauthorized', request.url))
|
||||
}
|
||||
|
||||
// 3. Add custom headers
|
||||
const response = NextResponse.next()
|
||||
response.headers.set('x-custom-header', 'value')
|
||||
response.headers.set('x-pathname', pathname)
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
export const advancedConfig = {
|
||||
matcher: ['/dashboard/:path*', '/admin/:path*'],
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 3: Request Rewriting
|
||||
// ============================================================================
|
||||
|
||||
export function rewriteProxy(request: NextRequest) {
|
||||
// Rewrite /blog/* to /posts/*
|
||||
if (request.nextUrl.pathname.startsWith('/blog')) {
|
||||
const url = request.nextUrl.clone()
|
||||
url.pathname = url.pathname.replace('/blog', '/posts')
|
||||
return NextResponse.rewrite(url)
|
||||
}
|
||||
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
export const rewriteConfig = {
|
||||
matcher: '/blog/:path*',
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 4: Geolocation-Based Routing
|
||||
// ============================================================================
|
||||
|
||||
export function geoProxy(request: NextRequest) {
|
||||
const country = request.geo?.country || 'US'
|
||||
const url = request.nextUrl.clone()
|
||||
|
||||
// Redirect to country-specific page
|
||||
if (url.pathname === '/') {
|
||||
url.pathname = `/${country.toLowerCase()}`
|
||||
return NextResponse.rewrite(url)
|
||||
}
|
||||
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 5: A/B Testing
|
||||
// ============================================================================
|
||||
|
||||
export function abTestProxy(request: NextRequest) {
|
||||
const bucket = request.cookies.get('bucket')?.value
|
||||
|
||||
if (!bucket) {
|
||||
// Assign to A or B randomly
|
||||
const newBucket = Math.random() < 0.5 ? 'a' : 'b'
|
||||
const response = NextResponse.next()
|
||||
response.cookies.set('bucket', newBucket, {
|
||||
maxAge: 60 * 60 * 24 * 30, // 30 days
|
||||
})
|
||||
|
||||
// Rewrite to variant page
|
||||
if (newBucket === 'b') {
|
||||
const url = request.nextUrl.clone()
|
||||
url.pathname = `/variant-b${url.pathname}`
|
||||
return NextResponse.rewrite(url)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// Existing user
|
||||
if (bucket === 'b') {
|
||||
const url = request.nextUrl.clone()
|
||||
url.pathname = `/variant-b${url.pathname}`
|
||||
return NextResponse.rewrite(url)
|
||||
}
|
||||
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
export const abTestConfig = {
|
||||
matcher: '/',
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 6: Rate Limiting
|
||||
// ============================================================================
|
||||
|
||||
const rateLimitMap = new Map<string, { count: number; resetAt: number }>()
|
||||
|
||||
export function rateLimitProxy(request: NextRequest) {
|
||||
const ip = request.headers.get('x-forwarded-for') || 'unknown'
|
||||
const now = Date.now()
|
||||
|
||||
// Check rate limit (100 requests per minute)
|
||||
const rateLimit = rateLimitMap.get(ip)
|
||||
|
||||
if (rateLimit) {
|
||||
if (now < rateLimit.resetAt) {
|
||||
if (rateLimit.count >= 100) {
|
||||
return new NextResponse('Too Many Requests', {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Retry-After': String(Math.ceil((rateLimit.resetAt - now) / 1000)),
|
||||
},
|
||||
})
|
||||
}
|
||||
rateLimit.count++
|
||||
} else {
|
||||
rateLimitMap.set(ip, { count: 1, resetAt: now + 60000 }) // 1 minute
|
||||
}
|
||||
} else {
|
||||
rateLimitMap.set(ip, { count: 1, resetAt: now + 60000 })
|
||||
}
|
||||
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
export const rateLimitConfig = {
|
||||
matcher: '/api/:path*',
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 7: Response Modification
|
||||
// ============================================================================
|
||||
|
||||
export function modifyResponseProxy(request: NextRequest) {
|
||||
const response = NextResponse.next()
|
||||
|
||||
// Add security headers
|
||||
response.headers.set('X-Frame-Options', 'DENY')
|
||||
response.headers.set('X-Content-Type-Options', 'nosniff')
|
||||
response.headers.set('Referrer-Policy', 'origin-when-cross-origin')
|
||||
response.headers.set(
|
||||
'Permissions-Policy',
|
||||
'camera=(), microphone=(), geolocation=()'
|
||||
)
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Migration Guide: middleware.ts → proxy.ts
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* ❌ BEFORE (Next.js 15):
|
||||
*
|
||||
* // File: middleware.ts
|
||||
* import { NextResponse } from 'next/server'
|
||||
* import type { NextRequest } from 'next/server'
|
||||
*
|
||||
* export function middleware(request: NextRequest) {
|
||||
* const token = request.cookies.get('token')
|
||||
* if (!token) {
|
||||
* return NextResponse.redirect(new URL('/login', request.url))
|
||||
* }
|
||||
* return NextResponse.next()
|
||||
* }
|
||||
*
|
||||
* export const config = {
|
||||
* matcher: '/dashboard/:path*',
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* ✅ AFTER (Next.js 16):
|
||||
*
|
||||
* // File: proxy.ts
|
||||
* import { NextResponse } from 'next/server'
|
||||
* import type { NextRequest } from 'next/server'
|
||||
*
|
||||
* export function proxy(request: NextRequest) {
|
||||
* const token = request.cookies.get('token')
|
||||
* if (!token) {
|
||||
* return NextResponse.redirect(new URL('/login', request.url))
|
||||
* }
|
||||
* return NextResponse.next()
|
||||
* }
|
||||
*
|
||||
* export const config = {
|
||||
* matcher: '/dashboard/:path*',
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* Migration Steps:
|
||||
* 1. Rename file: middleware.ts → proxy.ts
|
||||
* 2. Rename function: middleware → proxy
|
||||
* 3. Keep config object the same
|
||||
* 4. Logic remains identical
|
||||
*
|
||||
* Why the change?
|
||||
* - proxy.ts runs on Node.js runtime (full Node.js APIs)
|
||||
* - middleware.ts ran on Edge runtime (limited APIs)
|
||||
* - proxy.ts makes the network boundary explicit
|
||||
*
|
||||
* Note: middleware.ts still works in Next.js 16 but is deprecated.
|
||||
* Migrate to proxy.ts for future compatibility.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Summary:
|
||||
*
|
||||
* Proxy patterns:
|
||||
* 1. ✅ Auth checks and redirects
|
||||
* 2. ✅ Role-based access control
|
||||
* 3. ✅ Custom headers
|
||||
* 4. ✅ Request rewriting (URL rewrites)
|
||||
* 5. ✅ Geolocation-based routing
|
||||
* 6. ✅ A/B testing
|
||||
* 7. ✅ Rate limiting
|
||||
* 8. ✅ Response modification (security headers)
|
||||
*
|
||||
* Best practices:
|
||||
* - Keep proxy logic lightweight (runs on every request)
|
||||
* - Use matcher to limit scope
|
||||
* - Avoid database queries (use cookies/headers instead)
|
||||
* - Cache rate limit data in memory (or Redis for production)
|
||||
* - Return NextResponse.next() if no action needed
|
||||
*/
|
||||
393
templates/route-handler-api.ts
Normal file
393
templates/route-handler-api.ts
Normal file
@@ -0,0 +1,393 @@
|
||||
/**
|
||||
* Next.js 16 - Route Handlers (API Endpoints)
|
||||
*
|
||||
* Route Handlers replace API Routes from Pages Router.
|
||||
* File: app/api/[...]/route.ts
|
||||
*/
|
||||
|
||||
import { NextResponse } from 'next/server'
|
||||
import { cookies, headers } from 'next/headers'
|
||||
|
||||
// ============================================================================
|
||||
// Example 1: Basic CRUD API
|
||||
// ============================================================================
|
||||
|
||||
// GET /api/posts
|
||||
export async function GET() {
|
||||
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
|
||||
|
||||
return NextResponse.json(posts)
|
||||
}
|
||||
|
||||
// POST /api/posts
|
||||
export async function POST(request: Request) {
|
||||
const body = await request.json()
|
||||
|
||||
const post = await fetch('https://api.example.com/posts', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
}).then(r => r.json())
|
||||
|
||||
return NextResponse.json(post, { status: 201 })
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 2: Dynamic Routes
|
||||
// ============================================================================
|
||||
|
||||
// File: app/api/posts/[id]/route.ts
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id } = await params // ✅ Await params in Next.js 16
|
||||
|
||||
const post = await fetch(`https://api.example.com/posts/${id}`)
|
||||
.then(r => r.json())
|
||||
.catch(() => null)
|
||||
|
||||
if (!post) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Post not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(post)
|
||||
}
|
||||
|
||||
export async function PATCH(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id } = await params
|
||||
const body = await request.json()
|
||||
|
||||
const updated = await fetch(`https://api.example.com/posts/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
}).then(r => r.json())
|
||||
|
||||
return NextResponse.json(updated)
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id } = await params
|
||||
|
||||
await fetch(`https://api.example.com/posts/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
return NextResponse.json({ message: 'Post deleted' }, { status: 200 })
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 3: Search with Query Parameters
|
||||
// ============================================================================
|
||||
|
||||
// GET /api/search?q=nextjs&limit=10&page=1
|
||||
export async function SEARCH(request: Request) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const query = searchParams.get('q') || ''
|
||||
const limit = parseInt(searchParams.get('limit') || '10')
|
||||
const page = parseInt(searchParams.get('page') || '1')
|
||||
const offset = (page - 1) * limit
|
||||
|
||||
const results = await fetch(
|
||||
`https://api.example.com/search?q=${query}&limit=${limit}&offset=${offset}`
|
||||
).then(r => r.json())
|
||||
|
||||
return NextResponse.json({
|
||||
results: results.items,
|
||||
total: results.total,
|
||||
page,
|
||||
limit,
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 4: Authentication with Cookies
|
||||
// ============================================================================
|
||||
|
||||
// POST /api/auth/login
|
||||
export async function LOGIN(request: Request) {
|
||||
const { email, password } = await request.json()
|
||||
|
||||
// Verify credentials
|
||||
const user = await fetch('https://api.example.com/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
}).then(r => r.json())
|
||||
|
||||
if (!user.token) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid credentials' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// Set cookie
|
||||
const response = NextResponse.json({ success: true })
|
||||
response.cookies.set('token', user.token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict',
|
||||
maxAge: 60 * 60 * 24 * 7, // 7 days
|
||||
})
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// GET /api/auth/me
|
||||
export async function ME() {
|
||||
const cookieStore = await cookies() // ✅ Await cookies in Next.js 16
|
||||
const token = cookieStore.get('token')?.value
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const user = await fetch('https://api.example.com/auth/me', {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
}).then(r => r.json())
|
||||
|
||||
return NextResponse.json(user)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 5: Webhook Handler
|
||||
// ============================================================================
|
||||
|
||||
// File: app/api/webhooks/stripe/route.ts
|
||||
|
||||
import { headers as getHeaders } from 'next/headers'
|
||||
|
||||
export async function WEBHOOK(request: Request) {
|
||||
const body = await request.text()
|
||||
const headersList = await getHeaders() // ✅ Await headers in Next.js 16
|
||||
const signature = headersList.get('stripe-signature')
|
||||
|
||||
if (!signature) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing signature' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Verify webhook signature (example with Stripe)
|
||||
let event
|
||||
try {
|
||||
event = JSON.parse(body)
|
||||
// In production: stripe.webhooks.constructEvent(body, signature, secret)
|
||||
} catch (err) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid payload' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Handle event
|
||||
switch (event.type) {
|
||||
case 'payment_intent.succeeded':
|
||||
await handlePaymentSuccess(event.data.object)
|
||||
break
|
||||
case 'payment_intent.failed':
|
||||
await handlePaymentFailure(event.data.object)
|
||||
break
|
||||
default:
|
||||
console.log(`Unhandled event type: ${event.type}`)
|
||||
}
|
||||
|
||||
return NextResponse.json({ received: true })
|
||||
}
|
||||
|
||||
async function handlePaymentSuccess(paymentIntent: any) {
|
||||
console.log('Payment succeeded:', paymentIntent.id)
|
||||
// Update database, send confirmation email, etc.
|
||||
}
|
||||
|
||||
async function handlePaymentFailure(paymentIntent: any) {
|
||||
console.log('Payment failed:', paymentIntent.id)
|
||||
// Notify user, log error, etc.
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 6: Streaming Response
|
||||
// ============================================================================
|
||||
|
||||
// GET /api/stream
|
||||
export async function STREAM() {
|
||||
const encoder = new TextEncoder()
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const data = `data: ${JSON.stringify({ count: i })}\n\n`
|
||||
controller.enqueue(encoder.encode(data))
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
}
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 7: File Upload
|
||||
// ============================================================================
|
||||
|
||||
// POST /api/upload
|
||||
import { writeFile } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
|
||||
export async function UPLOAD(request: Request) {
|
||||
const formData = await request.formData()
|
||||
const file = formData.get('file') as File
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json(
|
||||
{ error: 'No file provided' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const bytes = await file.arrayBuffer()
|
||||
const buffer = Buffer.from(bytes)
|
||||
const filename = `${Date.now()}-${file.name}`
|
||||
const path = join(process.cwd(), 'public', 'uploads', filename)
|
||||
|
||||
await writeFile(path, buffer)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
url: `/uploads/${filename}`,
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 8: CORS Configuration
|
||||
// ============================================================================
|
||||
|
||||
export async function OPTIONS() {
|
||||
return new NextResponse(null, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function CORS_GET() {
|
||||
const response = NextResponse.json({ message: 'Hello' })
|
||||
|
||||
response.headers.set('Access-Control-Allow-Origin', '*')
|
||||
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
|
||||
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 9: Error Handling
|
||||
// ============================================================================
|
||||
|
||||
export async function ERROR_HANDLING() {
|
||||
try {
|
||||
const data = await fetch('https://api.example.com/data')
|
||||
.then(r => {
|
||||
if (!r.ok) throw new Error('API request failed')
|
||||
return r.json()
|
||||
})
|
||||
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Error:', error)
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal Server Error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 10: Rate Limiting
|
||||
// ============================================================================
|
||||
|
||||
const rateLimitMap = new Map<string, { count: number; resetAt: number }>()
|
||||
|
||||
export async function RATE_LIMITED() {
|
||||
const headersList = await headers()
|
||||
const ip = headersList.get('x-forwarded-for') || 'unknown'
|
||||
const now = Date.now()
|
||||
|
||||
const rateLimit = rateLimitMap.get(ip)
|
||||
|
||||
if (rateLimit) {
|
||||
if (now < rateLimit.resetAt) {
|
||||
if (rateLimit.count >= 10) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Too many requests' },
|
||||
{ status: 429 }
|
||||
)
|
||||
}
|
||||
rateLimit.count++
|
||||
} else {
|
||||
rateLimitMap.set(ip, { count: 1, resetAt: now + 60000 })
|
||||
}
|
||||
} else {
|
||||
rateLimitMap.set(ip, { count: 1, resetAt: now + 60000 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Success' })
|
||||
}
|
||||
|
||||
/**
|
||||
* Summary:
|
||||
*
|
||||
* Route Handlers (app/api/*/route.ts):
|
||||
* 1. ✅ Support all HTTP methods (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS)
|
||||
* 2. ✅ Await params in Next.js 16
|
||||
* 3. ✅ Access cookies with await cookies()
|
||||
* 4. ✅ Access headers with await headers()
|
||||
* 5. ✅ Use NextResponse.json() for JSON responses
|
||||
* 6. ✅ Return Response or NextResponse
|
||||
*
|
||||
* Common patterns:
|
||||
* - CRUD operations (GET, POST, PATCH, DELETE)
|
||||
* - Query parameters with searchParams
|
||||
* - Authentication with cookies
|
||||
* - Webhooks with signature verification
|
||||
* - Streaming responses (SSE, WebSocket)
|
||||
* - File uploads with FormData
|
||||
* - CORS configuration
|
||||
* - Error handling
|
||||
* - Rate limiting
|
||||
*
|
||||
* Best practices:
|
||||
* - Use try/catch for error handling
|
||||
* - Return appropriate HTTP status codes
|
||||
* - Validate input data
|
||||
* - Set secure cookie options in production
|
||||
* - Add rate limiting for public endpoints
|
||||
* - Use CORS headers when needed
|
||||
*/
|
||||
534
templates/server-actions-form.tsx
Normal file
534
templates/server-actions-form.tsx
Normal file
@@ -0,0 +1,534 @@
|
||||
/**
|
||||
* Next.js 16 - Server Actions for Form Handling
|
||||
*
|
||||
* This template shows comprehensive Server Actions patterns including:
|
||||
* - Basic form handling
|
||||
* - Validation with Zod
|
||||
* - Loading states
|
||||
* - Error handling
|
||||
* - Optimistic updates
|
||||
* - File uploads
|
||||
*/
|
||||
|
||||
'use server'
|
||||
|
||||
import { z } from 'zod'
|
||||
import { revalidateTag } from 'next/cache'
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
// ============================================================================
|
||||
// Example 1: Basic Server Action
|
||||
// ============================================================================
|
||||
|
||||
export async function createPost(formData: FormData) {
|
||||
const title = formData.get('title') as string
|
||||
const content = formData.get('content') as string
|
||||
|
||||
// Save to database
|
||||
await fetch('https://api.example.com/posts', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title, content }),
|
||||
})
|
||||
|
||||
// Revalidate cache
|
||||
revalidateTag('posts', 'max')
|
||||
|
||||
// Redirect to posts list
|
||||
redirect('/posts')
|
||||
}
|
||||
|
||||
// Basic form component
|
||||
export function CreatePostForm() {
|
||||
return (
|
||||
<form action={createPost}>
|
||||
<label>
|
||||
Title:
|
||||
<input type="text" name="title" required />
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Content:
|
||||
<textarea name="content" required />
|
||||
</label>
|
||||
|
||||
<button type="submit">Create Post</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 2: Server Action with Validation
|
||||
// ============================================================================
|
||||
|
||||
const PostSchema = z.object({
|
||||
title: z.string().min(3, 'Title must be at least 3 characters'),
|
||||
content: z.string().min(10, 'Content must be at least 10 characters'),
|
||||
tags: z.array(z.string()).min(1, 'At least one tag is required'),
|
||||
})
|
||||
|
||||
type ActionState = {
|
||||
errors?: {
|
||||
title?: string[]
|
||||
content?: string[]
|
||||
tags?: string[]
|
||||
_form?: string[]
|
||||
}
|
||||
success?: boolean
|
||||
}
|
||||
|
||||
export async function createPostWithValidation(
|
||||
prevState: ActionState,
|
||||
formData: FormData
|
||||
): Promise<ActionState> {
|
||||
// Parse form data
|
||||
const rawData = {
|
||||
title: formData.get('title'),
|
||||
content: formData.get('content'),
|
||||
tags: formData.get('tags')?.toString().split(',') || [],
|
||||
}
|
||||
|
||||
// Validate
|
||||
const parsed = PostSchema.safeParse(rawData)
|
||||
|
||||
if (!parsed.success) {
|
||||
return {
|
||||
errors: parsed.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
// Save to database
|
||||
try {
|
||||
await fetch('https://api.example.com/posts', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(parsed.data),
|
||||
})
|
||||
|
||||
revalidateTag('posts', 'max')
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
return {
|
||||
errors: {
|
||||
_form: ['Failed to create post. Please try again.'],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Form with validation errors
|
||||
'use client'
|
||||
|
||||
import { useFormState, useFormStatus } from 'react-dom'
|
||||
|
||||
function SubmitButton() {
|
||||
const { pending } = useFormStatus()
|
||||
|
||||
return (
|
||||
<button type="submit" disabled={pending}>
|
||||
{pending ? 'Creating...' : 'Create Post'}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export function ValidatedPostForm() {
|
||||
const [state, formAction] = useFormState(createPostWithValidation, {})
|
||||
|
||||
return (
|
||||
<form action={formAction}>
|
||||
<div>
|
||||
<label>
|
||||
Title:
|
||||
<input type="text" name="title" required />
|
||||
</label>
|
||||
{state.errors?.title && (
|
||||
<p className="error">{state.errors.title[0]}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Content:
|
||||
<textarea name="content" required />
|
||||
</label>
|
||||
{state.errors?.content && (
|
||||
<p className="error">{state.errors.content[0]}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Tags (comma-separated):
|
||||
<input type="text" name="tags" placeholder="nextjs, react, tutorial" />
|
||||
</label>
|
||||
{state.errors?.tags && (
|
||||
<p className="error">{state.errors.tags[0]}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{state.errors?._form && (
|
||||
<p className="error">{state.errors._form[0]}</p>
|
||||
)}
|
||||
|
||||
{state.success && (
|
||||
<p className="success">Post created successfully!</p>
|
||||
)}
|
||||
|
||||
<SubmitButton />
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 3: Server Action with Optimistic Updates
|
||||
// ============================================================================
|
||||
|
||||
'use server'
|
||||
|
||||
import { updateTag } from 'next/cache'
|
||||
|
||||
export async function likePost(postId: string) {
|
||||
await fetch(`https://api.example.com/posts/${postId}/like`, {
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
// Use updateTag for immediate refresh (read-your-writes)
|
||||
updateTag('posts')
|
||||
}
|
||||
|
||||
// Client component with optimistic updates
|
||||
'use client'
|
||||
|
||||
import { useOptimistic } from 'react'
|
||||
import { likePost } from './actions'
|
||||
|
||||
export function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
|
||||
const [optimisticLikes, addOptimisticLike] = useOptimistic(
|
||||
initialLikes,
|
||||
(state, amount: number) => state + amount
|
||||
)
|
||||
|
||||
async function handleLike() {
|
||||
// Update UI immediately
|
||||
addOptimisticLike(1)
|
||||
|
||||
// Sync with server
|
||||
await likePost(postId)
|
||||
}
|
||||
|
||||
return (
|
||||
<button onClick={handleLike}>
|
||||
❤️ {optimisticLikes} likes
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 4: Server Action with File Upload
|
||||
// ============================================================================
|
||||
|
||||
'use server'
|
||||
|
||||
import { writeFile } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
|
||||
export async function uploadImage(formData: FormData) {
|
||||
const file = formData.get('image') as File
|
||||
|
||||
if (!file) {
|
||||
return { error: 'No file provided' }
|
||||
}
|
||||
|
||||
// Validate file type
|
||||
const validTypes = ['image/jpeg', 'image/png', 'image/webp']
|
||||
if (!validTypes.includes(file.type)) {
|
||||
return { error: 'Invalid file type. Only JPEG, PNG, and WebP are allowed.' }
|
||||
}
|
||||
|
||||
// Validate file size (max 5MB)
|
||||
const maxSize = 5 * 1024 * 1024 // 5MB
|
||||
if (file.size > maxSize) {
|
||||
return { error: 'File too large. Maximum size is 5MB.' }
|
||||
}
|
||||
|
||||
// Save file
|
||||
const bytes = await file.arrayBuffer()
|
||||
const buffer = Buffer.from(bytes)
|
||||
const filename = `${Date.now()}-${file.name}`
|
||||
const path = join(process.cwd(), 'public', 'uploads', filename)
|
||||
|
||||
await writeFile(path, buffer)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
url: `/uploads/${filename}`,
|
||||
}
|
||||
}
|
||||
|
||||
// File upload form
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { uploadImage } from './actions'
|
||||
|
||||
export function ImageUploadForm() {
|
||||
const [preview, setPreview] = useState<string | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
async function handleSubmit(formData: FormData) {
|
||||
const result = await uploadImage(formData)
|
||||
|
||||
if (result.error) {
|
||||
setError(result.error)
|
||||
} else if (result.url) {
|
||||
setPreview(result.url)
|
||||
setError(null)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form action={handleSubmit}>
|
||||
<input
|
||||
type="file"
|
||||
name="image"
|
||||
accept="image/jpeg,image/png,image/webp"
|
||||
required
|
||||
/>
|
||||
|
||||
<button type="submit">Upload Image</button>
|
||||
|
||||
{error && <p className="error">{error}</p>}
|
||||
|
||||
{preview && (
|
||||
<div>
|
||||
<h3>Uploaded Image:</h3>
|
||||
<img src={preview} alt="Uploaded" width={300} />
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 5: Server Action with Progressive Enhancement
|
||||
// ============================================================================
|
||||
|
||||
'use server'
|
||||
|
||||
export async function subscribe(formData: FormData) {
|
||||
const email = formData.get('email') as string
|
||||
|
||||
await fetch('https://api.example.com/subscribe', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email }),
|
||||
})
|
||||
|
||||
return { success: true, message: 'Subscribed successfully!' }
|
||||
}
|
||||
|
||||
// Form works without JavaScript (progressive enhancement)
|
||||
export function SubscribeForm() {
|
||||
return (
|
||||
<form action={subscribe}>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="Enter your email"
|
||||
required
|
||||
/>
|
||||
<button type="submit">Subscribe</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
// Enhanced with JavaScript
|
||||
'use client'
|
||||
|
||||
import { useFormState } from 'react-dom'
|
||||
|
||||
export function EnhancedSubscribeForm() {
|
||||
const [state, formAction] = useFormState(subscribe, null)
|
||||
|
||||
return (
|
||||
<form action={formAction}>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="Enter your email"
|
||||
required
|
||||
/>
|
||||
<button type="submit">Subscribe</button>
|
||||
|
||||
{state?.success && (
|
||||
<p className="success">{state.message}</p>
|
||||
)}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 6: Server Action with Multi-Step Form
|
||||
// ============================================================================
|
||||
|
||||
'use server'
|
||||
|
||||
type Step1Data = { name: string; email: string }
|
||||
type Step2Data = { address: string; city: string }
|
||||
type FormData = Step1Data & Step2Data
|
||||
|
||||
export async function submitMultiStepForm(data: FormData) {
|
||||
// Validate all data
|
||||
const schema = z.object({
|
||||
name: z.string().min(2),
|
||||
email: z.string().email(),
|
||||
address: z.string().min(5),
|
||||
city: z.string().min(2),
|
||||
})
|
||||
|
||||
const parsed = schema.safeParse(data)
|
||||
|
||||
if (!parsed.success) {
|
||||
return {
|
||||
errors: parsed.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
// Save to database
|
||||
await fetch('https://api.example.com/users', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(parsed.data),
|
||||
})
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
// Multi-step form component
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
|
||||
export function MultiStepForm() {
|
||||
const [step, setStep] = useState(1)
|
||||
const [formData, setFormData] = useState<Partial<FormData>>({})
|
||||
|
||||
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault()
|
||||
|
||||
if (step === 1) {
|
||||
const form = e.currentTarget
|
||||
setFormData({
|
||||
...formData,
|
||||
name: (form.elements.namedItem('name') as HTMLInputElement).value,
|
||||
email: (form.elements.namedItem('email') as HTMLInputElement).value,
|
||||
})
|
||||
setStep(2)
|
||||
} else {
|
||||
const form = e.currentTarget
|
||||
const finalData = {
|
||||
...formData,
|
||||
address: (form.elements.namedItem('address') as HTMLInputElement).value,
|
||||
city: (form.elements.namedItem('city') as HTMLInputElement).value,
|
||||
} as FormData
|
||||
|
||||
const result = await submitMultiStepForm(finalData)
|
||||
|
||||
if (result.success) {
|
||||
alert('Form submitted successfully!')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
{step === 1 && (
|
||||
<>
|
||||
<h2>Step 1: Personal Info</h2>
|
||||
<input name="name" placeholder="Name" defaultValue={formData.name} required />
|
||||
<input name="email" type="email" placeholder="Email" defaultValue={formData.email} required />
|
||||
<button type="submit">Next</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{step === 2 && (
|
||||
<>
|
||||
<h2>Step 2: Address</h2>
|
||||
<input name="address" placeholder="Address" defaultValue={formData.address} required />
|
||||
<input name="city" placeholder="City" defaultValue={formData.city} required />
|
||||
<button type="button" onClick={() => setStep(1)}>Back</button>
|
||||
<button type="submit">Submit</button>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 7: Server Action with Rate Limiting
|
||||
// ============================================================================
|
||||
|
||||
'use server'
|
||||
|
||||
import { headers } from 'next/headers'
|
||||
|
||||
const rateLimitMap = new Map<string, { count: number; resetAt: number }>()
|
||||
|
||||
export async function submitContactForm(formData: FormData) {
|
||||
const headersList = await headers()
|
||||
const ip = headersList.get('x-forwarded-for') || 'unknown'
|
||||
|
||||
// Check rate limit (5 requests per hour)
|
||||
const now = Date.now()
|
||||
const rateLimit = rateLimitMap.get(ip)
|
||||
|
||||
if (rateLimit) {
|
||||
if (now < rateLimit.resetAt) {
|
||||
if (rateLimit.count >= 5) {
|
||||
return { error: 'Too many requests. Please try again later.' }
|
||||
}
|
||||
rateLimit.count++
|
||||
} else {
|
||||
// Reset rate limit
|
||||
rateLimitMap.set(ip, { count: 1, resetAt: now + 3600000 }) // 1 hour
|
||||
}
|
||||
} else {
|
||||
rateLimitMap.set(ip, { count: 1, resetAt: now + 3600000 })
|
||||
}
|
||||
|
||||
// Process form
|
||||
const name = formData.get('name') as string
|
||||
const message = formData.get('message') as string
|
||||
|
||||
await fetch('https://api.example.com/contact', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, message }),
|
||||
})
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
/**
|
||||
* Summary:
|
||||
*
|
||||
* Server Actions patterns:
|
||||
* 1. ✅ Basic form handling with formData
|
||||
* 2. ✅ Validation with Zod (safeParse)
|
||||
* 3. ✅ Loading states with useFormStatus
|
||||
* 4. ✅ Error handling with useFormState
|
||||
* 5. ✅ Optimistic updates with useOptimistic
|
||||
* 6. ✅ File uploads
|
||||
* 7. ✅ Progressive enhancement (works without JS)
|
||||
* 8. ✅ Multi-step forms
|
||||
* 9. ✅ Rate limiting
|
||||
*
|
||||
* Best practices:
|
||||
* - Always validate on server (never trust client input)
|
||||
* - Use revalidateTag() for background revalidation
|
||||
* - Use updateTag() for immediate refresh (forms, settings)
|
||||
* - Return errors instead of throwing (better UX)
|
||||
* - Use TypeScript for type safety
|
||||
* - Add rate limiting for public forms
|
||||
*/
|
||||
Reference in New Issue
Block a user