references/action-error-handling.md
---
title: Handle Server Action Errors Gracefully
impact: MEDIUM-HIGH
impactDescription: prevents silent failures, better error UX
tags: action, error-handling, validation, useActionState
---
## Handle Server Action Errors Gracefully
Return error states from Server Actions instead of throwing. Use `useActionState` to manage form state and display errors.
**Incorrect (unhandled errors):**
```typescript
async function createPost(formData: FormData) {
'use server'
const title = formData.get('title') as string
await db.posts.create({ data: { title } })
// If validation fails or DB errors, user sees nothing
}
```
**Correct (returning error state):**
```typescript
// actions.ts
'use server'
type ActionState = {
error?: string
success?: boolean
}
export async function createPost(
prevState: ActionState,
formData: FormData
): Promise<ActionState> {
const title = formData.get('title') as string
if (!title || title.length < 3) {
return { error: 'Title must be at least 3 characters' }
}
try {
await db.posts.create({ data: { title } })
revalidatePath('/posts')
return { success: true }
} catch (e) {
return { error: 'Failed to create post. Please try again.' }
}
}
// page.tsx
'use client'
import { useActionState } from 'react'
import { createPost } from './actions'
export default function NewPostForm() {
const [state, formAction, isPending] = useActionState(createPost, {})
return (
<form action={formAction}>
<input name="title" />
{state.error && <p className="error">{state.error}</p>}
<button disabled={isPending}>
{isPending ? 'Creating...' : 'Create'}
</button>
</form>
)
}
```
Reference: [useActionState](https://react.dev/reference/react/useActionState)
references/action-optimistic-updates.md
---
title: Use Optimistic Updates for Instant Feedback
impact: MEDIUM
impactDescription: instant UI response, better perceived performance
tags: action, optimistic, useOptimistic, mutation
---
## Use Optimistic Updates for Instant Feedback
Use `useOptimistic` to update UI immediately before the server confirms. If the action fails, React reverts to the previous state.
**Incorrect (waiting for server response):**
```typescript
'use client'
import { useState } from 'react'
export function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
const [likes, setLikes] = useState(initialLikes)
const [isLiking, setIsLiking] = useState(false)
async function handleLike() {
setIsLiking(true)
const newLikes = await likePost(postId) // Wait for server
setLikes(newLikes)
setIsLiking(false)
}
return (
<button onClick={handleLike} disabled={isLiking}>
{likes} {isLiking ? '...' : '❤️'}
</button>
)
}
// UI updates only after the server round trip.
```
**Correct (optimistic update):**
```typescript
'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, _) => state + 1
)
async function handleLike() {
addOptimisticLike(null) // Instant UI update
await likePost(postId) // Server update in background
// If fails, React reverts automatically
}
return (
<form action={handleLike}>
<button type="submit">
{optimisticLikes} ❤️
</button>
</form>
)
}
// Instant feedback, reverts on failure
```
**When to use:**
- Like/vote buttons
- Adding items to cart
- Toggling favorites
- Any action where instant feedback improves UX
references/action-pending-states.md
---
title: Show Pending States with useFormStatus
impact: MEDIUM-HIGH
impactDescription: better UX during form submission
tags: action, useFormStatus, pending, loading
---
## Show Pending States with useFormStatus
Use `useFormStatus` to show loading indicators and disable buttons during form submission. This provides immediate feedback to users.
**Incorrect (no feedback during submission):**
```typescript
// app/posts/new/page.tsx
export default function NewPostPage() {
async function createPost(formData: FormData) {
'use server'
await db.posts.create({ data: { title: formData.get('title') } })
}
return (
<form action={createPost}>
<input name="title" />
<button type="submit">Create Post</button>
{/* User clicks multiple times, no feedback */}
</form>
)
}
```
**Correct (pending state with useFormStatus):**
```typescript
// app/posts/new/page.tsx
import { SubmitButton } from './submit-button'
export default function NewPostPage() {
async function createPost(formData: FormData) {
'use server'
await db.posts.create({ data: { title: formData.get('title') } })
}
return (
<form action={createPost}>
<input name="title" />
<SubmitButton />
</form>
)
}
// submit-button.tsx
'use client'
import { useFormStatus } from 'react-dom'
export function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending}>
{pending ? 'Creating...' : 'Create Post'}
</button>
)
}
```
**Note:** `useFormStatus` must be used in a child component of the form, not in the same component as the form element.
references/action-revalidation.md
---
title: Revalidate Cache After Mutations
impact: MEDIUM
impactDescription: ensures fresh data after changes
tags: action, revalidation, cache, mutation
---
## Revalidate Cache After Mutations
After a mutation, invalidate only cached reads affected by the write. Use `revalidatePath` for route-level invalidation, `revalidateTag(tag, 'max')` for stale-while-revalidate content, or `updateTag(tag)` inside a Server Action for immediate read-your-writes behavior.
**Incorrect (stale cache after mutation):**
```typescript
'use server'
export async function deletePost(postId: string) {
await db.posts.delete({ where: { id: postId } })
redirect('/posts')
// Posts list still shows deleted post from cache!
}
```
**Correct (invalidating cache):**
```typescript
'use server'
import { revalidatePath, revalidateTag, updateTag } from 'next/cache'
import { redirect } from 'next/navigation'
export async function deletePost(postId: string) {
await db.posts.delete({ where: { id: postId } })
// Option 1: Revalidate specific path
revalidatePath('/posts')
// Option 2: expire and immediately refresh for this user's write
updateTag('posts')
redirect('/posts')
}
export async function updatePost(postId: string, formData: FormData) {
await db.posts.update({
where: { id: postId },
data: { title: formData.get('title') }
})
// Revalidate both the list and detail pages
revalidatePath('/posts')
revalidatePath(`/posts/${postId}`)
}
```
**Revalidation strategies:**
```typescript
// Specific route
revalidatePath('/posts')
// Dynamic route with specific ID
revalidatePath(`/posts/${postId}`)
// All routes using a layout
revalidatePath('/dashboard', 'layout')
// Stale-while-revalidate for webhook/content updates
revalidateTag('posts', 'max')
// Immediate read-your-writes in a Server Action
updateTag('posts')
updateTag(`post-${postId}`)
```
`updateTag` requires `cacheComponents: true` and can only be called from a Server Action. In Route Handlers and webhooks use `revalidateTag(tag, 'max')` instead.
Reference: [Next.js updating cached data](https://nextjs.org/docs/app/getting-started/caching-and-revalidating#updating-data)
references/action-server-action-forms.md
---
title: Use Server Actions for Form Submissions
impact: MEDIUM-HIGH
impactDescription: eliminates API routes, type-safe mutations
tags: action, server-action, form, mutation
---
## Use Server Actions for Form Submissions
Server Actions handle form submissions directly without creating API routes. They're type-safe, progressively enhanced, and integrate with caching.
**Incorrect (API route for form handling):**
```typescript
// app/api/posts/route.ts
export async function POST(request: Request) {
const data = await request.json()
const post = await db.posts.create({ data })
return Response.json(post)
}
// app/posts/new/page.tsx
'use client'
export default function NewPostPage() {
const handleSubmit = async (e) => {
e.preventDefault()
const formData = new FormData(e.target)
await fetch('/api/posts', {
method: 'POST',
body: JSON.stringify(Object.fromEntries(formData))
})
}
// Requires client component, manual fetch, no type safety
}
```
**Correct (Server Action):**
```typescript
// app/posts/new/page.tsx
import { redirect } from 'next/navigation'
import { revalidatePath } from 'next/cache'
export default function NewPostPage() {
async function createPost(formData: FormData) {
'use server'
const title = formData.get('title') as string
const content = formData.get('content') as string
const post = await db.posts.create({
data: { title, content }
})
revalidatePath('/posts')
redirect(`/posts/${post.id}`)
}
return (
<form action={createPost}>
<input name="title" required />
<textarea name="content" />
<button type="submit">Create Post</button>
</form>
)
}
// Works without JS, type-safe, integrated caching
```
**Benefits:**
- Progressive enhancement (works without JavaScript)
- Type-safe with TypeScript
- Direct cache invalidation
- No API route boilerplate
references/build-barrel-files.md
---
title: Avoid Barrel File Imports in App Router
impact: CRITICAL
impactDescription: 2-10× faster dev startup
tags: build, imports, barrel-files, tree-shaking
---
## Avoid Barrel File Imports in App Router
Barrel files (index.ts with re-exports) prevent tree-shaking and slow down development. Import directly from source files instead.
**Incorrect (imports through barrel file):**
```typescript
// lib/utils/index.ts (barrel file)
export * from './formatDate'
export * from './formatCurrency'
export * from './validateEmail'
// ... 50 more exports
// app/dashboard/page.tsx
import { formatDate } from '@/lib/utils'
// Loads all 50+ modules even though only formatDate is used
```
**Correct (direct import):**
```typescript
// app/dashboard/page.tsx
import { formatDate } from '@/lib/utils/formatDate'
// Loads only the formatDate module
```
**Alternative (path aliases):**
```typescript
// tsconfig.json
{
"compilerOptions": {
"paths": {
"@/utils/*": ["./lib/utils/*"]
}
}
}
// app/dashboard/page.tsx
import { formatDate } from '@/utils/formatDate'
```
**Note:** If you must use barrel files, configure `optimizePackageImports` or use explicit named exports instead of `export *`.
references/build-dynamic-imports.md
---
title: Use Dynamic Imports for Heavy Components
impact: CRITICAL
impactDescription: 30-70% smaller initial bundle
tags: build, dynamic-import, code-splitting, lazy-loading
---
## Use Dynamic Imports for Heavy Components
Large components like charts, editors, or maps should load on demand. Use `next/dynamic` to split them into separate bundles that load only when rendered.
**Incorrect (always included in main bundle):**
```typescript
import HeavyChart from '@/components/HeavyChart'
import CodeEditor from '@/components/CodeEditor'
export default function Dashboard() {
const [showChart, setShowChart] = useState(false)
return (
<div>
{showChart && <HeavyChart />}
<CodeEditor />
</div>
)
}
// Both components in initial bundle (~500KB added)
```
**Correct (loaded on demand):**
```typescript
'use client' // ssr: false is not allowed in Server Components
import { useState } from 'react'
import dynamic from 'next/dynamic'
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
loading: () => <ChartSkeleton />
})
const CodeEditor = dynamic(() => import('@/components/CodeEditor'), {
ssr: false // Client-only component
})
export default function Dashboard() {
const [showChart, setShowChart] = useState(false)
return (
<div>
{showChart && <HeavyChart />}
<CodeEditor />
</div>
)
}
// Components loaded only when rendered
```
**When to use `ssr: false`:** For libraries that touch `window` or `document` when imported, or when you also want the code split out of the server bundle. It only works from a Client Component. For a component that merely reads browser APIs while rendering, React 19.3's `use(browser())` inside `<Suspense>` skips its server render without a dynamic import (see the react skill's `effect-use-browser-only`).
Reference: [Dynamic Imports](https://nextjs.org/docs/app/building-your-application/optimizing/lazy-loading)
references/build-external-packages.md
---
title: Configure Server External Packages for Node Dependencies
impact: HIGH
impactDescription: prevents bundling issues, faster builds
tags: build, external-packages, node-modules, server
---
## Configure Server External Packages for Node Dependencies
Some Node.js packages with native bindings or complex dependencies should not be bundled. Use `serverExternalPackages` to exclude them from the server bundle.
**Incorrect (bundling native modules):**
```typescript
// next.config.ts
const nextConfig = {
// No external packages configured
}
// lib/pdf.ts
import puppeteer from 'puppeteer'
// Build fails or produces oversized bundles
```
**Correct (excluding native modules):**
```typescript
// next.config.ts
const nextConfig = {
serverExternalPackages: [
'puppeteer',
'sharp',
'canvas',
'@prisma/client',
'bcrypt'
]
}
// lib/pdf.ts
import puppeteer from 'puppeteer'
// Loaded at runtime from node_modules
```
**Common packages to externalize:**
- Database drivers: `@prisma/client`, `pg`, `mysql2`
- Image processing: `sharp`, `canvas`
- Native bindings: `bcrypt`, `argon2`
- Browser automation: `puppeteer`, `playwright`
references/build-optimize-package-imports.md
---
title: Configure optimizePackageImports for Icon Libraries
impact: CRITICAL
impactDescription: 200-800ms faster imports, 50-80% smaller bundles
tags: build, imports, tree-shaking, icons, turbopack
---
## Configure optimizePackageImports for Icon Libraries
Icon libraries like `lucide-react` export hundreds of modules. Without optimization, importing one icon loads the entire library. Configure `optimizePackageImports` to automatically tree-shake unused exports.
**Incorrect (loads entire library):**
```typescript
// next.config.ts
const nextConfig = {
// No optimization configured
}
// components/Header.tsx
import { Menu, X, Search } from 'lucide-react'
// Loads 1,583 modules, adds ~2.8s to dev startup
```
**Correct (loads only used icons):**
```typescript
// next.config.ts
const nextConfig = {
experimental: {
optimizePackageImports: ['lucide-react', '@heroicons/react', '@mui/icons-material']
}
}
// components/Header.tsx
import { Menu, X, Search } from 'lucide-react'
// Loads only 3 modules (~2KB vs ~1MB)
```
**Note:** Next.js 16 automatically optimizes common libraries. Add custom libraries that export many modules.
Reference: [How we optimized package imports in Next.js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)
references/build-turbopack-config.md
---
title: Configure Turbopack at the Top Level
impact: CRITICAL
impactDescription: uses the supported Next.js 16 bundler and cache configuration
tags: build, turbopack, caching, development
---
## Configure Turbopack at the Top Level
Next.js 16 uses Turbopack by default for both `next dev` and `next build`. Turbopack filesystem caching is also enabled by default for development and production builds. Do not add an opt-in flag merely to obtain the default behavior.
**Incorrect (removed or fictitious options):**
```typescript
const nextConfig = {
experimental: {
turbo: {
persistentCaching: true,
},
},
}
```
**Correct (only configure supported custom behavior):**
```typescript
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
turbopack: {
rules: {
'*.svg': {
loaders: ['@svgr/webpack'],
as: '*.js',
},
},
},
}
export default nextConfig
```
```bash
# Turbopack is the default in Next.js 16.
next dev
next build
# Use Webpack only when an unresolved compatibility requirement demands it.
next build --webpack
```
A custom `webpack` configuration makes the default `next build` fail rather than silently ignoring that configuration. Migrate the configuration, explicitly choose `--webpack`, or deliberately use `--turbopack` after confirming the Webpack customization is unnecessary.
The filesystem cache is local build output and must remain uncommitted. In Next.js 16.3+, both caches default to `true`. Set the documented experimental keys only to override those defaults for a demonstrated environment-specific reason:
```typescript
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
experimental: {
turbopackFileSystemCacheForDev: false,
turbopackFileSystemCacheForBuild: false,
},
}
export default nextConfig
```
References:
- [Next.js 16 upgrade guide](https://nextjs.org/docs/app/guides/upgrading/version-16#turbopack-by-default)
- [Turbopack filesystem cache](https://nextjs.org/docs/app/api-reference/config/next-config-js/turbopackFileSystemCache)
references/cache-fetch-options.md
---
title: Configure Fetch Cache Options Explicitly
impact: HIGH
impactDescription: makes runtime freshness and revalidation behavior deliberate
tags: cache, fetch, revalidate, data-fetching
---
## Configure Fetch Cache Options Explicitly
In Next.js 15 and 16, a server `fetch()` without an explicit cache policy is not cached at runtime. Build-time prerendering may still reuse it during the build. State the intended policy instead of relying on older defaults.
**Incorrect (assuming an implicit runtime cache):**
```typescript
export async function getProducts() {
// This is fresh at runtime in Next.js 15 and 16; it is not an implicit
// persistent cache entry.
return fetch('https://api.example.com/products')
}
```
**Correct (policies match each data contract):**
```typescript
export default async function Page() {
const config = await fetch('https://api.example.com/config', {
cache: 'force-cache',
})
const user = await fetch(`https://api.example.com/users/${userId}`, {
cache: 'no-store',
})
const products = await fetch('https://api.example.com/products', {
next: { revalidate: 300 },
})
const posts = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] },
})
}
```
Choose from the data contract:
- Request-specific, authorization-dependent, or real-time data: `cache: 'no-store'`.
- Reusable immutable data: `cache: 'force-cache'`.
- Reusable data with a bounded freshness window: `next: { revalidate: seconds }`.
- Reusable data requiring on-demand invalidation: attach `next.tags` and use the matching invalidation API.
Do not cache personalized responses under a key shared by users. Also distinguish the persistent Data Cache from request memoization: identical GET fetches can be deduplicated during one server render without becoming cross-request cached data.
Reference: [Next.js fetch API](https://nextjs.org/docs/app/api-reference/functions/fetch)
references/cache-react-cache.md
---
title: Use React cache() for Request Deduplication
impact: HIGH
impactDescription: eliminates duplicate fetches per request
tags: cache, react-cache, deduplication, server-components
---
## Use React cache() for Request Deduplication
Wrap data fetching functions with `cache()` to deduplicate identical calls within a single render pass. Multiple components can call the same function without triggering multiple fetches.
**Incorrect (duplicate fetches):**
```typescript
// lib/data.ts
export async function getUser(id: string) {
const res = await fetch(`/api/users/${id}`)
return res.json()
}
// components/Header.tsx
export async function Header({ userId }: { userId: string }) {
const user = await getUser(userId) // Fetch #1
return <h1>Welcome, {user.name}</h1>
}
// components/Sidebar.tsx
export async function Sidebar({ userId }: { userId: string }) {
const user = await getUser(userId) // Fetch #2 - duplicate!
return <nav>{user.role === 'admin' && <AdminLinks />}</nav>
}
```
**Correct (deduplicated with cache):**
```typescript
// lib/data.ts
import { cache } from 'react'
export const getUser = cache(async (id: string) => {
const res = await fetch(`/api/users/${id}`)
return res.json()
})
// components/Header.tsx
export async function Header({ userId }: { userId: string }) {
const user = await getUser(userId) // Fetch
return <h1>Welcome, {user.name}</h1>
}
// components/Sidebar.tsx
export async function Sidebar({ userId }: { userId: string }) {
const user = await getUser(userId) // Cached result reused
return <nav>{user.role === 'admin' && <AdminLinks />}</nav>
}
```
**Note:** React `cache()` deduplicates within a single request. For cross-request caching, use `unstable_cache` or the `'use cache'` directive.
references/cache-revalidate-path.md
---
title: Use revalidatePath for Route-Level Cache Invalidation
impact: HIGH
impactDescription: invalidates all cached data for a route
tags: cache, revalidate-path, invalidation, server-actions
---
## Use revalidatePath for Route-Level Cache Invalidation
Use `revalidatePath` to invalidate all cached data associated with a specific route. Prefer `revalidateTag` for granular control.
**Incorrect (forgetting to revalidate after mutation):**
```typescript
'use server'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
await db.posts.create({ data: { title, content } })
// User doesn't see new post until cache expires!
}
```
**Correct (revalidating after mutation):**
```typescript
'use server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
const post = await db.posts.create({ data: { title, content } })
revalidatePath('/posts') // Invalidate posts list
redirect(`/posts/${post.id}`) // Navigate to new post
}
```
**Path patterns:**
```typescript
// Specific route
revalidatePath('/posts')
// Dynamic route
revalidatePath('/posts/[slug]', 'page')
// Layout and all child routes
revalidatePath('/dashboard', 'layout')
// Entire app (use sparingly)
revalidatePath('/', 'layout')
```
**Note:** `redirect` must be called after `revalidatePath` as it throws internally.
references/cache-revalidate-tag.md
---
title: Use revalidateTag with cacheLife Profiles
impact: CRITICAL
impactDescription: stale-while-revalidate behavior, instant updates
tags: cache, revalidate-tag, cache-life, invalidation
---
## Use revalidateTag with cacheLife Profiles
Next.js 16 deprecates the single-argument `revalidateTag(tag)` form and reports it as a TypeScript error. Pass a cache-life profile such as `'max'` for stale-while-revalidate behavior, or choose `updateTag()` in a Server Action when the current user must immediately read their own write.
**Incorrect (old revalidateTag API):**
```typescript
// app/actions.ts
'use server'
import { revalidateTag } from 'next/cache'
export async function updateProduct(id: string, data: FormData) {
await db.products.update({ where: { id }, data })
// Deprecated single-argument API; fails current Next.js type checking.
revalidateTag('products')
}
```
**Correct (revalidateTag with cacheLife):**
```typescript
// app/actions.ts
'use server'
import { revalidateTag } from 'next/cache'
export async function updateProduct(id: string, data: FormData) {
await db.products.update({ where: { id }, data })
// Recommended: 'max' enables stale-while-revalidate
revalidateTag('products', 'max')
}
// 'max' (recommended): serve cached data immediately and refresh in the
// background the next time the tag is visited (stale-while-revalidate)
// Built-in cacheLife profiles: 'max', 'hours', 'days', 'weeks'
// For immediate read-your-writes updates use updateTag(tag) in a Server Action,
// or revalidateTag(tag, { expire: 0 }) for webhooks needing instant expiry
```
**Tagging cached data:**
```typescript
// lib/data.ts
'use cache'
import { cacheTag } from 'next/cache'
export async function getProducts() {
cacheTag('products')
const res = await fetch('https://api.store.com/products')
return res.json()
}
```
Reference: [Next.js revalidateTag](https://nextjs.org/docs/app/api-reference/functions/revalidateTag)
references/cache-segment-config.md
---
title: Match Route Configuration to the Active Cache Model
impact: MEDIUM-HIGH
impactDescription: avoids mixing incompatible Cache Components and legacy segment options
tags: cache, segment-config, dynamic, revalidate, cache-components
---
## Match Route Configuration to the Active Cache Model
Next.js 16 supports two caching models. First determine whether `cacheComponents: true` is enabled.
### Cache Components enabled
Use `'use cache'`, `cacheLife()`, `cacheTag()`, `<Suspense>`, and request-time APIs. Exporting `dynamic`, `revalidate`, `fetchCache`, `dynamicParams`, `runtime`, or `experimental_ppr` from a page, layout, or route is a build error when `cacheComponents` is enabled. Remove them rather than layering them onto the new model.
**Incorrect (legacy segment exports under Cache Components):**
```typescript
// app/about/page.tsx
export const dynamic = 'force-static'
export const revalidate = 86400
```
**Correct (Cache Components model):**
```typescript
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = { cacheComponents: true }
export default nextConfig
```
```typescript
// app/about/page.tsx
import { cacheLife } from 'next/cache'
export default async function AboutPage() {
'use cache'
cacheLife('days')
const team = await getTeam()
return <TeamSection team={team} />
}
```
### Cache Components disabled (previous model)
Route segment configuration remains available. Use it only when the whole segment genuinely shares that rendering policy; prefer per-fetch controls for mixed data.
```typescript
export const dynamic = 'force-static'
export const revalidate = 86400
export default async function AboutPage() {
const team = await getTeam()
return <TeamSection team={team} />
}
```
For request-specific routes in the previous model, use `dynamic = 'force-dynamic'` or explicit uncached fetches. `generateStaticParams()` controls which dynamic route parameters are generated; it is not itself a cache invalidation API.
Reference: [Next.js route segment config](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config)
references/cache-use-cache-directive.md
---
title: Use 'use cache' with Cache Components
impact: CRITICAL
impactDescription: opts stable work into the Next.js 16 Cache Components model
tags: cache, use-cache, directive, data-fetching, cache-components
---
## Use 'use cache' with Cache Components
The `'use cache'` directive is part of Next.js 16 Cache Components. Enable the model with top-level `cacheComponents: true`, then place the directive at file, component, or function scope for work whose inputs and outputs are serializable and safe to reuse.
**Incorrect (reading request-time data inside a cached scope):**
```typescript
import { cookies } from 'next/headers'
export async function getDashboard() {
'use cache'
const userId = (await cookies()).get('userId')?.value
return getDashboardForUser(userId)
}
```
Request-time APIs cannot be called inside a cached scope, and caching personalized output under shared arguments can leak data.
**Correct (enable Cache Components and cache stable inputs):**
```typescript
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = { cacheComponents: true }
export default nextConfig
```
```typescript
// lib/data.ts
import { cacheLife, cacheTag } from 'next/cache'
export async function getProducts() {
'use cache'
cacheLife('hours')
cacheTag('products')
const response = await fetch('https://api.store.com/products')
if (!response.ok) throw new Error('Failed to load products')
return response.json()
}
```
Without `'use cache'`, runtime fetches remain fresh by default. With it, the cached function follows its cache-life profile and can be invalidated through its tags; it is not necessarily cached forever.
Do not read request-time values such as `cookies()`, `headers()`, or `searchParams` inside a cached scope. Read them outside and pass only the minimal serializable value as an argument. Do not cache personalized data under shared arguments.
If `cacheComponents` is not enabled, follow the previous caching model (`fetch` cache options and, where needed, `unstable_cache`) rather than copying `'use cache'` into the app. Enabling Cache Components is a model change, not a rename-only migration.
Reference: [Next.js Cache Components](https://nextjs.org/docs/app/getting-started/cache-components)
references/client-children-pattern.md
---
title: Pass Server Components as Children to Client Components
impact: LOW-MEDIUM
impactDescription: keeps static content on server, reduces bundle
tags: client, children, composition, server-components
---
## Pass Server Components as Children to Client Components
Client Components can render Server Components passed as children. This keeps static content server-rendered while adding interactivity.
**Incorrect (converting children to Client Components):**
```typescript
// components/Modal.tsx
'use client'
import { ProductDetails } from './ProductDetails' // Forces this to be client
export function Modal({ productId }) {
const [isOpen, setIsOpen] = useState(false)
return (
<>
<button onClick={() => setIsOpen(true)}>View Details</button>
{isOpen && (
<div className="modal">
<ProductDetails productId={productId} /> {/* Now client-rendered */}
</div>
)}
</>
)
}
```
**Correct (children pattern keeps server content):**
```typescript
// components/Modal.tsx
'use client'
import { ReactNode, useState } from 'react'
export function Modal({ children, trigger }: { children: ReactNode; trigger: string }) {
const [isOpen, setIsOpen] = useState(false)
return (
<>
<button onClick={() => setIsOpen(true)}>{trigger}</button>
{isOpen && (
<div className="modal">
<button onClick={() => setIsOpen(false)}>Close</button>
{children} {/* Server Component passed as children */}
</div>
)}
</>
)
}
// app/product/[id]/page.tsx (Server Component)
export default async function ProductPage({ params }) {
const product = await getProduct(params.id)
return (
<Modal trigger="View Details">
<ProductDetails product={product} /> {/* Stays server-rendered */}
</Modal>
)
}
```
**Benefits:**
- `ProductDetails` remains a Server Component
- Data fetching happens on server
- Only Modal interactivity ships to client
references/client-hydration-mismatch.md
---
title: Avoid Hydration Mismatches
impact: LOW-MEDIUM
impactDescription: prevents React warnings, ensures correct rendering
tags: client, hydration, mismatch, ssr
---
## Avoid Hydration Mismatches
Server and client must render identical HTML. Avoid browser-only APIs, timestamps, and random values during initial render.
**Incorrect (hydration mismatch):**
```typescript
'use client'
export function Greeting() {
// Different on server vs client
const time = new Date().toLocaleTimeString()
return <p>Current time: {time}</p>
}
// Server renders "10:30:00", client hydrates with "10:30:01" → mismatch!
```
**Correct (defer client-only values):**
```typescript
'use client'
import { useState, useEffect } from 'react'
export function Greeting() {
const [time, setTime] = useState<string | null>(null)
useEffect(() => {
setTime(new Date().toLocaleTimeString())
const interval = setInterval(() => {
setTime(new Date().toLocaleTimeString())
}, 1000)
return () => clearInterval(interval)
}, [])
// Render nothing or placeholder on server
if (!time) return <p>Loading time...</p>
return <p>Current time: {time}</p>
}
```
**Correct (React 19.3+: skip server rendering for the component):**
```typescript
'use client'
import { Suspense, use, useEffect, useState } from 'react'
import { browser } from 'react-dom'
function Clock() {
use(browser('shows the user local time')) // server renders the fallback
const [time, setTime] = useState(() => new Date().toLocaleTimeString())
useEffect(() => {
const interval = setInterval(() => setTime(new Date().toLocaleTimeString()), 1000)
return () => clearInterval(interval)
}, [])
return <p>Current time: {time}</p>
}
export function Greeting() {
return (
<Suspense fallback={<p>Loading time...</p>}>
<Clock />
</Suspense>
)
}
```
This drops the `null` state and the extra render. The effect remains only for the interval. See the react skill's `effect-use-browser-only`.
**Alternative (suppressHydrationWarning for known differences):**
```typescript
'use client'
export function Timestamp() {
return (
<time suppressHydrationWarning>
{new Date().toLocaleTimeString()}
</time>
)
}
// Use sparingly - only when mismatch is intentional
```
**Common causes:**
- `Date.now()`, `Math.random()`
- `window.innerWidth`, `navigator.userAgent`
- Browser extensions modifying HTML
- Different locales on server/client
references/client-third-party-scripts.md
---
title: Load Third-Party Scripts Efficiently
impact: LOW-MEDIUM
impactDescription: prevents blocking, improves LCP
tags: client, scripts, third-party, performance
---
## Load Third-Party Scripts Efficiently
Use `next/script` for third-party scripts with appropriate loading strategies. Avoid blocking the main thread with synchronous scripts.
**Incorrect (blocking script in head):**
```typescript
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html>
<head>
<script src="https://analytics.example.com/script.js" />
{/* Blocks rendering until script loads */}
</head>
<body>{children}</body>
</html>
)
}
```
**Correct (next/script with strategy):**
```typescript
// app/layout.tsx
import Script from 'next/script'
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
{/* Analytics - load after page is interactive */}
<Script
src="https://analytics.example.com/script.js"
strategy="afterInteractive"
/>
{/* Chat widget - load when idle */}
<Script
src="https://chat.example.com/widget.js"
strategy="lazyOnload"
/>
{/* Rare critical bootstrap that must run before hydration */}
<Script id="consent-bootstrap" strategy="beforeInteractive">
{`window.consentConfig = { region: 'auto' };`}
</Script>
</body>
</html>
)
}
```
**Strategy guide:**
- `beforeInteractive` - Critical scripts (rare)
- `afterInteractive` - Analytics, tracking (default)
- `lazyOnload` - Chat widgets, social buttons
- `worker` - Experimental and version/router constrained; verify support in the installed Next.js docs
references/client-use-client-boundary.md
---
title: Minimize 'use client' Boundary Scope
impact: LOW-MEDIUM
impactDescription: reduces client JS, better performance
tags: client, use-client, boundary, hydration
---
## Minimize 'use client' Boundary Scope
Keep `'use client'` boundaries as small as possible. Only the interactive parts need to be Client Components.
**Incorrect (entire page as Client Component):**
```typescript
'use client'
export default function ProductPage({ product }) {
const [quantity, setQuantity] = useState(1)
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p> {/* Static, doesn't need client */}
<img src={product.image} /> {/* Static, doesn't need client */}
<Reviews reviews={product.reviews} /> {/* Static, doesn't need client */}
{/* Only this needs interactivity */}
<input value={quantity} onChange={e => setQuantity(+e.target.value)} />
<button onClick={() => addToCart(product.id, quantity)}>Add to Cart</button>
</div>
)
}
// Entire page hydrates on client
```
**Correct (minimal Client Component):**
```typescript
// app/product/[id]/page.tsx (Server Component)
export default async function ProductPage({ params }) {
const product = await getProduct(params.id)
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<img src={product.image} />
<Reviews reviews={product.reviews} />
{/* Only interactive part is client */}
<AddToCartButton productId={product.id} />
</div>
)
}
// components/AddToCartButton.tsx
'use client'
import { useState } from 'react'
export function AddToCartButton({ productId }: { productId: string }) {
const [quantity, setQuantity] = useState(1)
return (
<div>
<input value={quantity} onChange={e => setQuantity(+e.target.value)} />
<button onClick={() => addToCart(productId, quantity)}>Add to Cart</button>
</div>
)
}
// Only button hydrates, rest is static HTML
```
references/meta-generate-metadata.md
---
title: Use generateMetadata for Dynamic Metadata
impact: MEDIUM
impactDescription: dynamic SEO, social sharing optimization
tags: meta, generateMetadata, seo, dynamic
---
## Use generateMetadata for Dynamic Metadata
Export `generateMetadata` to create dynamic metadata based on route parameters and fetched data. This enables unique titles, descriptions, and OpenGraph images per page.
**Incorrect (static metadata for dynamic pages):**
```typescript
// app/product/[id]/page.tsx
export const metadata = {
title: 'Product', // Same for all products!
description: 'View product details'
}
export default async function ProductPage({ params }) {
const product = await getProduct(params.id)
return <ProductDetails product={product} />
}
```
**Correct (dynamic metadata per product):**
```typescript
// app/product/[id]/page.tsx
import type { Metadata } from 'next'
export async function generateMetadata({
params
}: {
params: { id: string }
}): Promise<Metadata> {
const product = await getProduct(params.id)
return {
title: product.name,
description: product.description,
openGraph: {
title: product.name,
description: product.description,
images: [
{
url: product.image,
width: 1200,
height: 630,
alt: product.name
}
]
},
twitter: {
card: 'summary_large_image',
title: product.name,
description: product.description,
images: [product.image]
}
}
}
export default async function ProductPage({ params }) {
const product = await getProduct(params.id) // Deduplicated with cache()
return <ProductDetails product={product} />
}
```
**Note:** Next.js automatically deduplicates `fetch` calls, so `generateMetadata` and the page can call `getProduct` without duplicate requests.
references/meta-opengraph-images.md
---
title: Generate Dynamic OpenGraph Images
impact: LOW-MEDIUM
impactDescription: better social sharing, higher CTR
tags: meta, opengraph, image, social
---
## Generate Dynamic OpenGraph Images
Use `opengraph-image.tsx` to generate dynamic social preview images. This creates unique, branded images for each page.
**Incorrect (missing or generic OG images):**
```typescript
// No OG image configured
// Social shares show generic placeholder or nothing
```
**Correct (dynamic OG image generation):**
```typescript
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og'
export const runtime = 'edge'
export const alt = 'Blog post cover'
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'
export default async function Image({
params
}: {
params: { slug: string }
}) {
const post = await getPost(params.slug)
return new ImageResponse(
(
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
height: '100%',
backgroundColor: '#1a1a1a',
color: 'white',
padding: '40px'
}}
>
<h1 style={{ fontSize: '60px', textAlign: 'center' }}>
{post.title}
</h1>
<p style={{ fontSize: '30px', color: '#888' }}>
{post.author} · {post.readTime} min read
</p>
</div>
),
{ ...size }
)
}
```
**Static fallback for routes without dynamic image:**
```typescript
// app/opengraph-image.png
// Place a static image in the route for default OG image
```
Reference: [OpenGraph Images](https://nextjs.org/docs/app/api-reference/file-conventions/metadata/opengraph-image)
references/meta-robots.md
---
title: Configure Robots for Crawl Control
impact: MEDIUM
impactDescription: prevents indexing of private pages
tags: meta, robots, seo, crawling
---
## Configure Robots for Crawl Control
Use `robots.ts` and per-page robots metadata to control which pages search engines can crawl and index.
**Incorrect (no robots configuration):**
```typescript
// No robots.ts
// Search engines may index admin pages, staging URLs, etc.
```
**Correct (robots.ts for global rules):**
```typescript
// app/robots.ts
import type { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: ['/admin/', '/api/', '/dashboard/']
}
],
sitemap: `${baseUrl}/sitemap.xml`
}
}
```
**Per-page robots metadata:**
```typescript
// app/dashboard/page.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
robots: {
index: false,
follow: false,
nocache: true,
googleBot: {
index: false,
follow: false
}
}
}
export default function DashboardPage() {
// Private dashboard content
}
```
**Common patterns:**
- `index: false` - Don't show in search results
- `follow: false` - Don't follow links on this page
- `nocache` - Don't cache this page
- `noarchive` - Don't show cached version in results
references/meta-sitemap.md
---
title: Generate Sitemaps Dynamically
impact: MEDIUM
impactDescription: improved crawlability, faster indexing
tags: meta, sitemap, seo, crawling
---
## Generate Sitemaps Dynamically
Create dynamic sitemaps that include all your pages with proper last-modified dates. This helps search engines discover and index content efficiently.
**Incorrect (static sitemap missing dynamic routes):**
```xml
<!-- public/sitemap.xml -->
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://example.com/</loc>
</url>
<url>
<loc>https://example.com/about</loc>
</url>
</urlset>
<!-- Missing all product pages! -->
```
**Correct (dynamic sitemap.ts):**
```typescript
// app/sitemap.ts
import type { MetadataRoute } from 'next'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const products = await getProducts()
const posts = await getPosts()
const productUrls = products.map((product) => ({
url: `https://example.com/products/${product.slug}`,
lastModified: product.updatedAt,
changeFrequency: 'weekly' as const,
priority: 0.8
}))
const postUrls = posts.map((post) => ({
url: `https://example.com/blog/${post.slug}`,
lastModified: post.updatedAt,
changeFrequency: 'monthly' as const,
priority: 0.6
}))
return [
{
url: 'https://example.com',
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1
},
...productUrls,
...postUrls
]
}
```
**For large sites (50,000+ URLs), split into multiple sitemaps:**
```typescript
// app/sitemap/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const page = parseInt(params.id)
const products = await getProductsPage(page, 10000)
// Generate sitemap XML for this page
}
```
references/route-ensure-static.md
---
title: Guard Static Routes with ensureStatic
impact: MEDIUM-HIGH
impactDescription: fails the build instead of shipping request-time rendering to a static route
tags: route, static, ensure-static, cache-components, partial-prefetching, segment-config
---
## Guard Static Routes with ensureStatic
> **Next.js 16.4+.** Requires `cacheComponents: true`; the build fails otherwise. `'shell'` and `'prefetch'` also need `partialPrefetching: true` (without it they have no effect and Next.js warns).
Cache Components lets one page stream static and dynamic content together. For blogs, marketing pages and stores that should stay static, one dynamic component (a `cookies()` read, an uncached fetch) can silently turn the route into per-request rendering. Export `ensureStatic` from a page or layout to make that a build error.
**Incorrect (a dynamic component sneaks into a page meant to be static):**
```tsx
// app/blog/[slug]/page.tsx
export default function Page() {
return (
<>
<UserAvatar /> {/* reads cookies(): request-time rendering, no error */}
<Content />
</>
)
}
```
**Correct (the route must be fully static):**
```tsx
// app/blog/[slug]/page.tsx
export const ensureStatic = 'navigation'
export async function generateStaticParams() {
return [{ slug: 'hello-world' }]
}
export default function Page() {
return <Content /> // adding <UserAvatar /> now fails next dev and next build
}
```
**Allowed values** (`'auto' | 'shell' | 'prefetch' | 'navigation' | false`; each stricter level includes the earlier stages):
| Value | Output that must be static |
| --- | --- |
| `'auto'` (default) | No additional requirement; same as omitting the export |
| `'shell'` | App Shell |
| `'prefetch'` | App Shell and per-link prefetch |
| `'navigation'` | Complete server-rendered route (works with Cache Components alone) |
| `false` | Not a static level; fails the build if another segment sets `'shell'`, `'prefetch'` or `'navigation'` |
**Apply it to a whole app from the root layout:**
```tsx
// app/layout.tsx
export const ensureStatic = 'navigation'
```
A child segment may set a stricter level than its parent but not a weaker one (the build fails), and `false` cannot be mixed with a static level in a parent and child. If one section needs dynamic content, set the level on the other sections' layouts instead of the root. Parallel slots rendered together must agree.
**Notes:**
- With `'navigation'`, a dynamic route must export `generateStaticParams()` returning complete parameter sets.
- `'navigation'` rejects uncached data, `cookies()`, `headers()`, server `searchParams`, and `use cache` scopes with `expire` under 5 minutes, `stale` under 30 seconds or `revalidate: 0`, even inside `<Suspense>`. `await navigation()` and `await prefetch()` are allowed.
- Client Components may load request data in the browser (`use(browser())`, `useParams()`, `useSearchParams()`).
- Cannot be exported from a Client Component. It enforces static output; it does not validate instant UI (that is `instant`).
- Per-subtree alternatives: see `navigation()` and `prefetch()` in [route-partial-prefetching](route-partial-prefetching.md).
Reference: [ensureStatic](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config/ensureStatic) · [Next.js 16.4](https://nextjs.org/blog/next-16-4)
references/route-instant-navigations.md
---
title: Make Navigations Instant with Stream, Cache, or Block
impact: HIGH
impactDescription: SPA-like instant navigations in a server-driven app
tags: route, navigation, instant, suspense, use-cache, cache-components, instant-insights
---
## Make Navigations Instant with Stream, Cache, or Block
> **Next.js 16.3+ (the `instant` export is stable since 16.3; Cache Components is recommended and on by default in `create-next-app` since 16.4).** Requires `cacheComponents: true` in `next.config.ts`. Both flags (`cacheComponents`, `partialPrefetching`) become defaults in Next.js 17.
In a server-driven app, a navigation that `await`s data on the server blocks until the network roundtrip completes: click → nothing → page. Next.js makes slow navigations a development error (the **Instant Insights** panel) and gives you three ways to resolve each one:
- **Stream** with `<Suspense>` — the user instantly sees a loading state, then UI streams in.
- **Cache** with `'use cache'` — the user instantly sees previously cached UI reused between requests.
- **Block** with `export const instant = false` — deliberately keep the navigation server-bound (e.g., a blog that never shows a loading shell for posts).
**Incorrect (accidental blocking navigation):**
```typescript
// app/products/[id]/page.tsx
export default async function ProductPage({ params }) {
const { id } = await params
const product = await getProduct(id) // No Suspense, no 'use cache'
return <ProductDetails product={product} />
}
// Click → blank frame until the server roundtrip finishes.
// Instant Insights reports this as a slow navigation in dev.
```
**Correct (stream for instant shell):**
```typescript
// app/products/[id]/page.tsx
import { Suspense } from 'react'
export default async function ProductPage({ params }) {
const { id } = await params
return (
<Suspense fallback={<ProductSkeleton />}>
<ProductDetails productId={id} />
</Suspense>
)
}
// Click → skeleton renders instantly, details stream in.
```
**Correct (cache for instant cached UI):**
```typescript
// app/products/[id]/data.ts
export async function getProduct(id: string) {
'use cache'
const product = await db.product.findUnique({ where: { id } })
return product
}
// Repeat visits render instantly from the cached entry.
```
**Correct (explicit opt-out when the route must block):**
```typescript
// app/blog/[slug]/page.tsx
export const instant = false
export default async function PostPage({ params }) {
const post = await getPost((await params).slug)
return <Article post={post} />
}
// Navigations stay server-bound by choice; the dev error goes away.
```
**Catch regressions with the Playwright `instant` helper:**
```typescript
import { expect, test } from '@playwright/test'
import { instant } from '@next/playwright'
test('product title is available immediately', async ({ page }) => {
await page.goto('/products/shoes')
// Assert what is visible without waiting for the network
await instant(page, async () => {
await page.click('a[href="/products/hats"]')
await expect(page.locator('h1')).toContainText('Baseball Cap')
await expect(page.getByText('Checking inventory...')).toBeVisible()
})
await expect(page.getByText('12 in stock')).toBeVisible()
})
```
**Notes:**
- Enable with `cacheComponents: true`; combine with `partialPrefetching: true` for the shell prefetching that makes the first click instant (see [route-partial-prefetching](route-partial-prefetching.md)).
- The **Navigation Inspector** in Next.js DevTools pauses a navigation at the shell so you can see what displays instantly versus after the network hop.
- Instant Insights validation runs in development only and the `level` option currently accepts only `'warning'`. Dev prefetching is not performed, so verify feel on `next start`.
references/route-intercepting-routes.md
---
title: Use Intercepting Routes for Modal Patterns
impact: HIGH
impactDescription: enables shareable modal URLs, better UX
tags: route, intercepting-routes, modal, navigation
---
## Use Intercepting Routes for Modal Patterns
Intercepting routes display content in a modal when navigating client-side, while showing the full page on direct access or refresh. Perfect for image galleries, login modals, and detail views.
**Incorrect (client-state modal without URL):**
```typescript
'use client'
export default function PhotoGallery({ photos }) {
const [selectedPhoto, setSelectedPhoto] = useState(null)
return (
<div>
{photos.map(photo => (
<Image
key={photo.id}
onClick={() => setSelectedPhoto(photo)}
/>
))}
{selectedPhoto && (
<Modal onClose={() => setSelectedPhoto(null)}>
<PhotoDetail photo={selectedPhoto} />
</Modal>
)}
</div>
)
}
// Modal not shareable, lost on refresh
```
**Correct (intercepting route):**
```text
app/
├── @modal/
│ ├── (.)photo/[id]/
│ │ └── page.tsx # Shows in modal on client nav
│ └── default.tsx
├── photo/[id]/
│ └── page.tsx # Shows full page on direct access
└── page.tsx # Gallery
```
```typescript
// app/@modal/(.)photo/[id]/page.tsx
import { Modal } from '@/components/Modal'
export default async function PhotoModal({ params }: { params: { id: string } }) {
const photo = await getPhoto(params.id)
return (
<Modal>
<PhotoDetail photo={photo} />
</Modal>
)
}
// app/photo/[id]/page.tsx
export default async function PhotoPage({ params }: { params: { id: string } }) {
const photo = await getPhoto(params.id)
return <PhotoDetail photo={photo} /> // Full page
}
```
**Interception conventions:**
- `(.)` - Same level
- `(..)` - One level up
- `(..)(..)` - Two levels up
- `(...)` - From root
references/route-not-found.md
---
title: Use notFound() for Missing Resources
impact: MEDIUM
impactDescription: proper 404 handling, better SEO
tags: route, not-found, 404, error-handling
---
## Use notFound() for Missing Resources
Call `notFound()` when a dynamic resource doesn't exist. This renders the closest `not-found.tsx` and returns a proper 404 status code.
**Incorrect (rendering empty state for missing data):**
```typescript
// app/product/[id]/page.tsx
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id)
if (!product) {
return <div>Product not found</div> // Returns 200, bad for SEO
}
return <ProductDetail product={product} />
}
```
**Correct (using notFound()):**
```typescript
// app/product/[id]/page.tsx
import { notFound } from 'next/navigation'
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id)
if (!product) {
notFound() // Returns 404, renders not-found.tsx
}
return <ProductDetail product={product} />
}
// app/product/[id]/not-found.tsx
export default function ProductNotFound() {
return (
<div>
<h2>Product Not Found</h2>
<p>The product you're looking for doesn't exist.</p>
<Link href="/products">Browse all products</Link>
</div>
)
}
```
**Benefits:**
- Correct 404 HTTP status for SEO
- Crawlers understand the page doesn't exist
- Custom UI for missing resources
- Can be nested per route segment
references/route-parallel-routes.md
---
title: Use Parallel Routes for Independent Content
impact: HIGH
impactDescription: independent loading, streaming, error handling
tags: route, parallel-routes, slots, layout
---
## Use Parallel Routes for Independent Content
Parallel routes (slots) render multiple pages simultaneously in the same layout. Each slot can have independent loading, error, and streaming states.
**Incorrect (sequential rendering in single page):**
```typescript
// app/dashboard/page.tsx
export default async function DashboardPage() {
const analytics = await fetchAnalytics() // Slow
const notifications = await fetchNotifications()
const activity = await fetchActivity()
return (
<div className="grid grid-cols-3">
<Analytics data={analytics} />
<Notifications data={notifications} />
<Activity data={activity} />
</div>
)
}
// All sections wait for slowest fetch
```
**Correct (parallel routes with independent streaming):**
```text
app/dashboard/
├── layout.tsx
├── @analytics/
│ ├── page.tsx
│ └── loading.tsx
├── @notifications/
│ ├── page.tsx
│ └── loading.tsx
└── @activity/
├── page.tsx
└── loading.tsx
```
```typescript
// app/dashboard/layout.tsx
export default function DashboardLayout({
analytics,
notifications,
activity
}: {
analytics: React.ReactNode
notifications: React.ReactNode
activity: React.ReactNode
}) {
return (
<div className="grid grid-cols-3">
{analytics}
{notifications}
{activity}
</div>
)
}
// app/dashboard/@analytics/page.tsx
export default async function AnalyticsSlot() {
const data = await fetchAnalytics()
return <Analytics data={data} />
}
// Each slot streams independently
```
**Benefits:**
- Each slot loads independently
- Each slot has its own loading.tsx
- Each slot can have its own error.tsx
references/route-partial-prefetching.md
---
title: Use Partial Prefetching for Reusable Route Shells
impact: HIGH
impactDescription: one prefetch per route instead of per link, instant first click
tags: route, prefetching, partial-prefetching, shell, link, navigation
---
## Use Partial Prefetching for Reusable Route Shells
> **Next.js 16.3+ (stable in 16.4).** Requires `cacheComponents: true` and `partialPrefetching: true` in `next.config.ts`. As of 16.4 Partial Prefetching is part of the Cache Components model, and both flags become defaults in Next.js 17. Leaving `partialPrefetching` unset while `cacheComponents` is on logs a warning.
Before 16.3, Next.js sent a prefetch request for **every link** in the viewport — even many links to the same route, producing a flurry of requests on scroll. Partial Prefetching replaces this with a single-page-app trick: **prefetch one reusable loading shell per route**, fetched once and cached on the client for the session.
```typescript
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
partialPrefetching: true,
}
export default nextConfig
```
**Incorrect (relying on per-link prefetch for responsiveness):**
```typescript
// A sidebar with twenty chat links fires a prefetch per link,
// most of them for the same /chat/[id] route
export function Sidebar({ chats }) {
return (
<nav>
{chats.map((chat) => (
<Link key={chat.id} href={`/chat/${chat.id}`}>
{chat.name}
</Link>
))}
</nav>
)
}
// Wasteful: the same route shell is fetched once per link
```
**Correct (let the shell be prefetched once per route):**
```typescript
// With partialPrefetching enabled, Next.js prefetches a single
// reusable shell for /chat/[id], plus one per other distinct route
export function Sidebar({ chats }) {
return (
<nav>
{chats.map((chat) => (
<Link key={chat.id} href={`/chat/${chat.id}`}>
{chat.name}
</Link>
))}
</nav>
)
}
// One request for the route, cached across all twenty links
```
**Prefetch more than the shell for specific links:**
```tsx
// Opt a single link into deeper prefetching so the chat header
// "pops in" instantly instead of waiting for the network
<Link href={`/chat/${chat.id}`} prefetch={true}>
{chat.name}
</Link>
```
With `<Link prefetch={true}>`, Next.js still does not render the entire route. It renders down to content that is:
- available synchronously,
- known from the URL (`params`, `searchParams`), or
- marked with `'use cache'`.
This removes the all-or-nothing choice: the instant shell is the baseline, and `<Link prefetch={true}>` plus `'use cache'` adds per-link depth only where it pays off.
**Defer expensive work out of the shell or the prefetch (16.4):**
Two functions from `next/cache` mark where a subtree stops being part of an earlier navigation stage. Both take no arguments, return `Promise<void>`, require `cacheComponents`, and must be called outside any `'use cache'` scope and outside Client Components. Wrap the awaiting component in `<Suspense>` so a fallback renders in the earlier stage.
```tsx
import { Suspense } from 'react'
import { navigation, prefetch } from 'next/cache'
async function Thread({ id }: { id: string }) {
await navigation() // excluded from the App Shell AND per-link prefetches
return <ThreadList items={await getThread(id)} />
}
async function Recommendations() {
await prefetch() // excluded from the App Shell; may load in a per-link prefetch
return <List items={await getRecommendations()} />
}
```
- `await navigation()`: content renders only when the user actually navigates. Use it so `<Link prefetch={true}>` does not load an expensive tail (for example a whole message thread) for every visible link.
- `await prefetch()`: content stays out of the shared App Shell but can load in a per-link `prefetch={true}` or on navigation.
- Unlike `connection()`, neither marks the subtree request-dependent, so work below them can still use `'use cache'`. Put the directive on a function called below the await.
- To enforce a stage across a whole route instead of per subtree, see [route-ensure-static](route-ensure-static.md).
**Notes:**
- Inspect what a shell contains with the **Navigation Inspector** in Next.js DevTools — it pauses each navigation at the shell so you can see what renders instantly versus after the network hop.
- Actual prefetching happens only in production builds; in development the Inspector simulates it.
- Because shells are reused across links, they are the foundation for upcoming offline navigation support.
- Pair with [route-instant-navigations](route-instant-navigations.md) (Stream / Cache / Block) so each route actually has an instant shell to prefetch.
references/route-prefetching.md
---
title: Configure Link Prefetching Appropriately
impact: MEDIUM-HIGH
impactDescription: instant navigation for likely destinations
tags: route, prefetching, link, navigation
---
## Configure Link Prefetching Appropriately
Next.js automatically prefetches linked routes. Control this behavior based on route importance and user likelihood to navigate.
**Incorrect (no prefetch consideration):**
```typescript
// Prefetches all links, including rarely used ones
export default function Navigation() {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/products">Products</Link>
<Link href="/admin/settings">Settings</Link> {/* Rarely accessed */}
<Link href="/terms">Terms</Link> {/* Rarely accessed */}
</nav>
)
}
// Wastes bandwidth prefetching unlikely routes
```
**Correct (strategic prefetching):**
```typescript
export default function Navigation() {
return (
<nav>
{/* High-traffic routes - prefetch (default) */}
<Link href="/">Home</Link>
<Link href="/products">Products</Link>
{/* Low-traffic routes - disable prefetch */}
<Link href="/admin/settings" prefetch={false}>Settings</Link>
<Link href="/terms" prefetch={false}>Terms</Link>
</nav>
)
}
```
**Prefetch on hover for conditional routes:**
```typescript
'use client'
import { useRouter } from 'next/navigation'
export function ProductCard({ product }) {
const router = useRouter()
return (
<div
onMouseEnter={() => router.prefetch(`/product/${product.id}`)}
onClick={() => router.push(`/product/${product.id}`)}
>
{product.name}
</div>
)
}
// Prefetches only when user shows intent
```
**Note:** In production, prefetching only loads the shared layout and static portions of the route.
**Next.js 16.3+ changes this model (stable in 16.4, default in 17):** with `partialPrefetching: true`, Next.js prefetches one reusable shell per route (fetched once, cached across links) instead of per link, and `prefetch={true}` becomes an opt-in for deeper per-link prefetching. Slow navigations become dev errors under `cacheComponents: true` — see [route-partial-prefetching](route-partial-prefetching.md) and [route-instant-navigations](route-instant-navigations.md).
references/route-proxy-ts.md
---
title: Use proxy.ts for Network Boundary Logic
impact: MEDIUM-HIGH
impactDescription: clearer network boundary, Node.js runtime
tags: route, proxy, middleware, network
---
## Use proxy.ts for Network Boundary Logic
Next.js 16 deprecates the `middleware.ts` convention in favor of `proxy.ts` for network-boundary logic. `proxy.ts` uses the Node.js runtime and its runtime cannot be configured. If an existing boundary requires the Edge runtime, keep `middleware.ts` until the installed Next.js version provides a documented migration path.
**Incorrect (old middleware.ts pattern):**
```typescript
// middleware.ts (deprecated in Next.js 16)
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))
}
}
export const config = {
matcher: '/dashboard/:path*'
}
```
**Correct (proxy.ts in Next.js 16):**
```typescript
// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
const token = request.cookies.get('token')
// Full Node.js APIs available (not Edge)
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
// Add headers, rewrite, etc.
const response = NextResponse.next()
response.headers.set('x-custom-header', 'value')
return response
}
export const config = {
matcher: '/dashboard/:path*'
}
```
**Migration:**
1. Rename `middleware.ts` → `proxy.ts`
2. Rename exported function `middleware` → `proxy`
3. Remove Edge-only assumptions and verify Node.js runtime behavior; otherwise retain `middleware.ts` for the Edge requirement
Reference: [Next.js proxy file convention](https://nextjs.org/docs/app/api-reference/file-conventions/proxy)
references/server-avoid-client-fetching.md
---
title: Avoid Client-Side Data Fetching for Initial Data
impact: MEDIUM-HIGH
impactDescription: eliminates client waterfalls, better SEO
tags: server, data-fetching, seo, waterfalls
---
## Avoid Client-Side Data Fetching for Initial Data
Fetch initial page data in Server Components, not with `useEffect` or client-side libraries. Client-side fetching creates waterfalls and hurts SEO.
**Incorrect (client-side fetch with useEffect):**
```typescript
'use client'
import { useState, useEffect } from 'react'
export default function ProductsPage() {
const [products, setProducts] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch('/api/products')
.then(res => res.json())
.then(data => {
setProducts(data)
setLoading(false)
})
}, [])
if (loading) return <Skeleton />
return <ProductList products={products} />
}
// Waterfall: HTML → JS → Hydrate → Fetch → Render
// Empty for SEO crawlers
```
**Correct (Server Component fetch):**
```typescript
// app/products/page.tsx
export default async function ProductsPage() {
const products = await fetch('https://api.store.com/products')
.then(res => res.json())
return <ProductList products={products} />
}
// Single request, data included in HTML, SEO-friendly
```
**When to use client-side fetching:**
- User-initiated actions (load more, search)
- Real-time updates (polling, WebSocket)
- After-interaction data (comments on expand)
**Recommended client-side library:**
```typescript
'use client'
import useSWR from 'swr'
export function SearchResults({ query }: { query: string }) {
const { data, isLoading } = useSWR(
query ? `/api/search?q=${query}` : null,
fetcher
)
// Client fetch appropriate for user-initiated search
}
```
references/server-component-streaming.md
---
title: Stream Server Components for Progressive Loading
impact: HIGH
impactDescription: faster Time to First Byte, progressive rendering
tags: server, streaming, suspense, progressive-loading
---
## Stream Server Components for Progressive Loading
Split data-intensive Server Components and wrap them in Suspense to stream HTML progressively. Fast components render immediately while slow ones load.
**Incorrect (all-or-nothing rendering):**
```typescript
// app/page.tsx
export default async function Page() {
const user = await fetchUser() // 100ms
const posts = await fetchPosts() // 500ms
const analytics = await fetchAnalytics() // 2000ms
return (
<div>
<Header user={user} />
<PostList posts={posts} />
<Analytics data={analytics} />
</div>
)
}
// Nothing renders until analytics completes (2100ms)
```
**Correct (progressive streaming):**
```typescript
// app/page.tsx
import { Suspense } from 'react'
export default function Page() {
return (
<div>
<Suspense fallback={<HeaderSkeleton />}>
<Header />
</Suspense>
<Suspense fallback={<PostsSkeleton />}>
<PostList />
</Suspense>
<Suspense fallback={<AnalyticsSkeleton />}>
<Analytics />
</Suspense>
</div>
)
}
// Each component fetches its own data
async function Header() {
const user = await fetchUser()
return <header>{user.name}</header>
}
async function Analytics() {
const data = await fetchAnalytics()
return <AnalyticsChart data={data} />
}
// Header renders in 100ms, Posts in 500ms, Analytics in 2000ms
```
**Benefits:**
- First paint happens immediately
- Each section appears as soon as its data is ready
- Slow components don't block fast ones
references/server-data-colocation.md
---
title: Colocate Data Fetching with Components
impact: HIGH
impactDescription: eliminates prop drilling, enables streaming
tags: server, colocation, data-fetching, architecture
---
## Colocate Data Fetching with Components
Fetch data where it's needed, not in parent components. This enables independent streaming and eliminates prop drilling.
**Incorrect (fetching in parent, prop drilling):**
```typescript
// app/dashboard/page.tsx
export default async function DashboardPage() {
const user = await fetchUser()
const orders = await fetchOrders(user.id)
const notifications = await fetchNotifications(user.id)
return (
<Dashboard>
<Header user={user} />
<OrderList orders={orders} user={user} />
<NotificationPanel notifications={notifications} userId={user.id} />
</Dashboard>
)
}
// All data fetched sequentially, no streaming possible
```
**Correct (colocated data fetching):**
```typescript
// app/dashboard/page.tsx
import { Suspense } from 'react'
export default function DashboardPage() {
return (
<Dashboard>
<Suspense fallback={<HeaderSkeleton />}>
<Header />
</Suspense>
<Suspense fallback={<OrdersSkeleton />}>
<OrderList />
</Suspense>
<Suspense fallback={<NotificationsSkeleton />}>
<NotificationPanel />
</Suspense>
</Dashboard>
)
}
// components/OrderList.tsx
async function OrderList() {
const user = await getUser() // Deduplicated with cache()
const orders = await fetchOrders(user.id)
return <ul>{orders.map(o => <OrderItem key={o.id} order={o} />)}</ul>
}
// Each component fetches what it needs, streams independently
```
**Note:** Use `cache()` to deduplicate shared data like user across components.
references/server-error-handling.md
---
title: Handle Server Component Errors Gracefully
impact: MEDIUM
impactDescription: prevents full page crashes, better UX
tags: server, error-handling, error-boundary, resilience
---
## Handle Server Component Errors Gracefully
Use error boundaries and try/catch to handle failures gracefully. A single failed fetch shouldn't crash the entire page.
**Incorrect (unhandled error crashes page):**
```typescript
// app/dashboard/page.tsx
export default async function DashboardPage() {
const analytics = await fetchAnalytics() // If this fails, entire page crashes
return (
<div>
<Header />
<Analytics data={analytics} />
</div>
)
}
```
**Correct (graceful error handling):**
```typescript
// app/dashboard/page.tsx
import { Suspense } from 'react'
import { ErrorBoundary } from 'react-error-boundary'
export default function DashboardPage() {
return (
<div>
<Header />
<ErrorBoundary fallback={<AnalyticsError />}>
<Suspense fallback={<AnalyticsSkeleton />}>
<Analytics />
</Suspense>
</ErrorBoundary>
</div>
)
}
// Or use error.tsx for route-level errors
// app/dashboard/error.tsx
'use client'
export default function Error({
error,
reset
}: {
error: Error
reset: () => void
}) {
return (
<div>
<h2>Something went wrong</h2>
<button onClick={() => reset()}>Try again</button>
</div>
)
}
```
**Try/catch for specific components:**
```typescript
async function Analytics() {
try {
const data = await fetchAnalytics()
return <AnalyticsChart data={data} />
} catch (error) {
return <AnalyticsUnavailable />
}
}
```
references/server-parallel-fetching.md
---
title: Fetch Data in Parallel in Server Components
impact: HIGH
impactDescription: eliminates server-side waterfalls, 2-5× faster
tags: server, parallel, data-fetching, promise-all
---
## Fetch Data in Parallel in Server Components
Initiate all independent data fetches simultaneously using `Promise.all()`. Sequential awaits create server-side waterfalls that multiply latency.
**Incorrect (sequential fetches, 3 round trips):**
```typescript
// app/dashboard/page.tsx
export default async function DashboardPage() {
const user = await fetchUser() // 200ms
const orders = await fetchOrders() // 150ms
const notifications = await fetchNotifications() // 100ms
// Total: 450ms (sequential)
return (
<Dashboard
user={user}
orders={orders}
notifications={notifications}
/>
)
}
```
**Correct (parallel fetches, 1 round trip):**
```typescript
// app/dashboard/page.tsx
export default async function DashboardPage() {
const [user, orders, notifications] = await Promise.all([
fetchUser(), // 200ms
fetchOrders(), // 150ms (parallel)
fetchNotifications() // 100ms (parallel)
])
// Total: 200ms (longest request)
return (
<Dashboard
user={user}
orders={orders}
notifications={notifications}
/>
)
}
```
**With dependent data:**
```typescript
export default async function DashboardPage() {
// First fetch user (needed for subsequent queries)
const user = await fetchUser()
// Then fetch user-dependent data in parallel
const [orders, preferences] = await Promise.all([
fetchOrders(user.id),
fetchPreferences(user.id)
])
return <Dashboard user={user} orders={orders} preferences={preferences} />
}
```
references/server-preload-pattern.md
---
title: Use Preload Pattern for Critical Data
impact: MEDIUM-HIGH
impactDescription: starts fetches earlier in render tree
tags: server, preload, data-fetching, optimization
---
## Use Preload Pattern for Critical Data
Export a `preload` function that initiates data fetching at the top of the component tree. This starts fetches earlier, reducing time to first byte.
**Incorrect (data fetch starts late in component tree):**
```typescript
// app/product/[id]/page.tsx
export default async function ProductPage({ params }: { params: { id: string } }) {
return (
<div>
<Header />
<Breadcrumbs />
<ProductDetails id={params.id} /> {/* Fetch starts here */}
</div>
)
}
async function ProductDetails({ id }: { id: string }) {
const product = await getProduct(id) // Fetch delayed by parent render
return <div>{product.name}</div>
}
```
**Correct (preload starts fetch immediately):**
```typescript
// lib/data.ts
import { cache } from 'react'
export const getProduct = cache(async (id: string) => {
const res = await fetch(`/api/products/${id}`)
return res.json()
})
export const preloadProduct = (id: string) => {
void getProduct(id) // Start fetch, don't await
}
// app/product/[id]/page.tsx
import { preloadProduct, getProduct } from '@/lib/data'
export default async function ProductPage({ params }: { params: { id: string } }) {
preloadProduct(params.id) // Start fetch immediately
return (
<div>
<Header />
<Breadcrumbs />
<ProductDetails id={params.id} />
</div>
)
}
async function ProductDetails({ id }: { id: string }) {
const product = await getProduct(id) // Uses cached promise
return <div>{product.name}</div>
}
```
**Note:** The `cache()` wrapper ensures the preloaded data is reused by child components.
references/stream-error-tsx.md
---
title: Use error.tsx for Route-Level Error Boundaries
impact: MEDIUM
impactDescription: graceful error recovery, prevents full page crashes
tags: stream, error, error-boundary, recovery
---
## Use error.tsx for Route-Level Error Boundaries
Create `error.tsx` files to catch errors in route segments. Users can retry without navigating away or losing state.
**Incorrect (unhandled errors crash the page):**
```typescript
// app/dashboard/page.tsx
export default async function DashboardPage() {
const data = await fetchData() // If this throws, entire app crashes
return <Dashboard data={data} />
}
```
**Correct (error.tsx catches and recovers):**
```typescript
// app/dashboard/error.tsx
'use client' // Error components must be Client Components
import { useEffect } from 'react'
export default function DashboardError({
error,
reset
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
// Log to error reporting service
console.error(error)
}, [error])
return (
<div className="p-4 bg-red-50 rounded">
<h2>Something went wrong loading the dashboard</h2>
<button
onClick={() => reset()}
className="mt-2 px-4 py-2 bg-red-600 text-white rounded"
>
Try again
</button>
</div>
)
}
```
**Error boundary hierarchy:**
```text
app/
├── error.tsx # Catches errors in all routes
├── global-error.tsx # Catches errors in root layout
└── dashboard/
├── error.tsx # Catches errors in dashboard routes only
└── page.tsx
```
**Note:** `error.tsx` doesn't catch errors in the same segment's `layout.tsx`. Place `error.tsx` in the parent segment to catch layout errors.
references/stream-loading-tsx.md
---
title: Use loading.tsx for Route-Level Loading States
impact: MEDIUM
impactDescription: automatic loading UI, instant navigation feedback
tags: stream, loading, skeleton, navigation
---
## Use loading.tsx for Route-Level Loading States
Create `loading.tsx` files to show instant loading UI during route transitions. Next.js automatically wraps the page in Suspense with this component as fallback.
**Incorrect (no loading state):**
```text
app/dashboard/
└── page.tsx
# Navigation to /dashboard shows blank screen until data loads
```
**Correct (loading.tsx for instant feedback):**
```text
app/dashboard/
├── loading.tsx
└── page.tsx
```
```typescript
// app/dashboard/loading.tsx
export default function DashboardLoading() {
return (
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-1/4 mb-4" />
<div className="grid grid-cols-3 gap-4">
<div className="h-32 bg-gray-200 rounded" />
<div className="h-32 bg-gray-200 rounded" />
<div className="h-32 bg-gray-200 rounded" />
</div>
</div>
)
}
// app/dashboard/page.tsx
export default async function DashboardPage() {
const data = await fetchDashboardData()
return <Dashboard data={data} />
}
```
**Best practices:**
- Match skeleton structure to actual content
- Use CSS animations for polish
- Keep skeletons lightweight (no data fetching)
- Nest loading.tsx for granular control
```text
app/dashboard/
├── loading.tsx # Dashboard skeleton
├── page.tsx
└── analytics/
├── loading.tsx # Analytics-specific skeleton
└── page.tsx
```
references/stream-nested-suspense.md
---
title: Nest Suspense for Progressive Disclosure
impact: LOW-MEDIUM
impactDescription: fine-grained loading control, better UX
tags: stream, suspense, nested, progressive
---
## Nest Suspense for Progressive Disclosure
Nest Suspense boundaries to create progressive loading experiences. Outer boundaries show first, inner boundaries refine as data loads.
**Incorrect (flat Suspense structure):**
```typescript
export default function ProductPage() {
return (
<div>
<Suspense fallback={<ProductSkeleton />}>
<ProductDetails />
</Suspense>
<Suspense fallback={<ReviewsSkeleton />}>
<Reviews />
</Suspense>
<Suspense fallback={<RelatedSkeleton />}>
<RelatedProducts />
</Suspense>
</div>
)
}
// All sections load independently, no visual hierarchy
```
**Correct (nested progressive disclosure):**
```typescript
export default function ProductPage() {
return (
<div>
{/* Product details load first - critical content */}
<Suspense fallback={<ProductSkeleton />}>
<ProductDetails />
{/* Reviews load after product - secondary content */}
<Suspense fallback={<ReviewsSkeleton />}>
<Reviews />
{/* Related products load last - tertiary content */}
<Suspense fallback={<RelatedSkeleton />}>
<RelatedProducts />
</Suspense>
</Suspense>
</Suspense>
</div>
)
}
// Content reveals progressively: Product → Reviews → Related
```
**Alternative (prioritized parallel loading):**
```typescript
export default function ProductPage() {
return (
<div>
{/* Critical path - no Suspense, blocks render */}
<ProductHeader />
<div className="grid grid-cols-2 gap-8">
{/* Primary content */}
<Suspense fallback={<DetailsSkeleton />}>
<ProductDetails />
</Suspense>
{/* Secondary content - lower priority */}
<Suspense fallback={<SidebarSkeleton />}>
<ProductSidebar />
</Suspense>
</div>
</div>
)
}
```
references/stream-skeleton-matching.md
---
title: Match Skeleton Dimensions to Actual Content
impact: MEDIUM
impactDescription: prevents layout shift, better CLS score
tags: stream, skeleton, layout-shift, cls
---
## Match Skeleton Dimensions to Actual Content
Loading skeletons should match the dimensions of actual content to prevent layout shift (CLS). Use fixed heights or aspect ratios.
**Incorrect (skeleton causes layout shift):**
```typescript
// loading.tsx
export default function Loading() {
return <div className="h-8 w-full bg-gray-200 animate-pulse" />
}
// page.tsx
export default async function Page() {
const data = await fetchData()
return (
<div className="h-64"> {/* Height doesn't match skeleton */}
<Content data={data} />
</div>
)
}
// Page jumps from 32px to 256px when content loads
```
**Correct (skeleton matches content dimensions):**
```typescript
// loading.tsx
export default function Loading() {
return (
<div className="space-y-4">
{/* Header skeleton - matches actual header height */}
<div className="h-12 w-64 bg-gray-200 animate-pulse rounded" />
{/* Card grid skeleton - matches actual card dimensions */}
<div className="grid grid-cols-3 gap-4">
{[1, 2, 3].map(i => (
<div key={i} className="h-48 bg-gray-200 animate-pulse rounded" />
))}
</div>
</div>
)
}
// page.tsx
export default async function Page() {
const data = await fetchData()
return (
<div className="space-y-4">
<h1 className="h-12 text-3xl">{data.title}</h1>
<div className="grid grid-cols-3 gap-4">
{data.cards.map(card => (
<Card key={card.id} className="h-48" {...card} />
))}
</div>
</div>
)
}
// No layout shift - skeleton and content have same dimensions
```
**Tips:**
- Use the same CSS classes for skeleton and content containers
- Set explicit heights on dynamic content
- Use `aspect-ratio` for images and videos
references/stream-suspense-boundaries.md
---
title: Place Suspense Boundaries Strategically
impact: MEDIUM
impactDescription: faster perceived performance, progressive loading
tags: stream, suspense, loading, progressive
---
## Place Suspense Boundaries Strategically
Wrap slow components in Suspense to show meaningful content immediately. Place boundaries around content that fetches data independently.
**Incorrect (single Suspense for entire page):**
```typescript
// app/dashboard/page.tsx
import { Suspense } from 'react'
export default function DashboardPage() {
return (
<Suspense fallback={<FullPageLoader />}>
<Dashboard />
</Suspense>
)
}
// User sees full-page spinner until everything loads
```
**Correct (granular Suspense boundaries):**
```typescript
// app/dashboard/page.tsx
import { Suspense } from 'react'
export default function DashboardPage() {
return (
<div className="grid grid-cols-3 gap-4">
{/* Fast content renders immediately */}
<Header />
{/* Each section loads independently */}
<Suspense fallback={<StatsSkeleton />}>
<StatsWidget />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<RecentOrders />
</Suspense>
</div>
)
}
// Header shows instantly, widgets stream in as they load
```
**Guidelines for Suspense boundaries:**
- Wrap each independent data-fetching component
- Group related components in single boundary
- Keep fallbacks similar in size to actual content (prevent layout shift)
- Prioritize above-the-fold content
SKILL.md
---
name: nextjs
description: Next.js 16 App Router caching and routing.
---
# Next.js Community Next.js 16 App Router Best Practices
Comprehensive performance optimization guide for Next.js 16 App Router applications, maintained by the Next.js Community. Contains 43 rules across 8 categories, prioritized by impact to guide automated refactoring and code generation. Current: Next.js 16.4 (16.4.5), where Cache Components with Partial Prefetching is the recommended model and `create-next-app` enables it by default. Three rules cover it (Instant Navigations, Partial Prefetching, `ensureStatic`), gated behind `cacheComponents` and `partialPrefetching`.
## When to Apply
Reference these guidelines when:
- Writing new Next.js 16 App Router code
- Configuring caching strategies with 'use cache' directive
- Implementing server components and data fetching
- Setting up routing with parallel and intercepting routes
- Creating server actions for form handling and mutations
## Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|----------|----------|--------|--------|
| 1 | Build & Bundle Optimization | CRITICAL | `build-` |
| 2 | Caching Strategy | CRITICAL | `cache-` |
| 3 | Server Components & Data Fetching | HIGH | `server-` |
| 4 | Routing & Navigation | HIGH | `route-` |
| 5 | Server Actions & Mutations | MEDIUM-HIGH | `action-` |
| 6 | Streaming & Loading States | MEDIUM | `stream-` |
| 7 | Metadata & SEO | MEDIUM | `meta-` |
| 8 | Client Components | LOW-MEDIUM | `client-` |
## Quick Reference
### 1. Build & Bundle Optimization (CRITICAL)
- `build-optimize-package-imports` - Configure optimizePackageImports for Icon Libraries
- `build-dynamic-imports` - Use Dynamic Imports for Heavy Components
- `build-barrel-files` - Avoid Barrel File Imports in App Router
- `build-turbopack-config` - Configure Turbopack at the top level
- `build-external-packages` - Configure Server External Packages for Node Dependencies
### 2. Caching Strategy (CRITICAL)
- `cache-use-cache-directive` - Use 'use cache' with Cache Components
- `cache-revalidate-tag` - Use revalidateTag with cacheLife Profiles
- `cache-fetch-options` - Configure Fetch Cache Options Explicitly
- `cache-revalidate-path` - Use revalidatePath for Route-Level Cache Invalidation
- `cache-react-cache` - Use React cache() for Request Deduplication
- `cache-segment-config` - Match route configuration to the active cache model
### 3. Server Components & Data Fetching (HIGH)
- `server-parallel-fetching` - Fetch Data in Parallel in Server Components
- `server-component-streaming` - Stream Server Components for Progressive Loading
- `server-data-colocation` - Colocate Data Fetching with Components
- `server-preload-pattern` - Use Preload Pattern for Critical Data
- `server-avoid-client-fetching` - Avoid Client-Side Data Fetching for Initial Data
- `server-error-handling` - Handle Server Component Errors Gracefully
### 4. Routing & Navigation (HIGH)
- `route-parallel-routes` - Use Parallel Routes for Independent Content
- `route-intercepting-routes` - Use Intercepting Routes for Modal Patterns
- `route-prefetching` - Configure Link Prefetching Appropriately
- `route-instant-navigations` - Make Navigations Instant with Stream, Cache, or Block (16.3+)
- `route-partial-prefetching` - Use Partial Prefetching for Reusable Route Shells, plus `navigation()` / `prefetch()` (16.4)
- `route-ensure-static` - Guard Static Routes with ensureStatic (16.4)
- `route-proxy-ts` - Use proxy.ts for Network Boundary Logic
- `route-not-found` - Use notFound() for Missing Resources
### 5. Server Actions & Mutations (MEDIUM-HIGH)
- `action-server-action-forms` - Use Server Actions for Form Submissions
- `action-pending-states` - Show Pending States with useFormStatus
- `action-error-handling` - Handle Server Action Errors Gracefully
- `action-optimistic-updates` - Use Optimistic Updates for Instant Feedback
- `action-revalidation` - Revalidate Cache After Mutations
### 6. Streaming & Loading States (MEDIUM)
- `stream-suspense-boundaries` - Place Suspense Boundaries Strategically
- `stream-loading-tsx` - Use loading.tsx for Route-Level Loading States
- `stream-error-tsx` - Use error.tsx for Route-Level Error Boundaries
- `stream-skeleton-matching` - Match Skeleton Dimensions to Actual Content
- `stream-nested-suspense` - Nest Suspense for Progressive Disclosure
### 7. Metadata & SEO (MEDIUM)
- `meta-generate-metadata` - Use generateMetadata for Dynamic Metadata
- `meta-sitemap` - Generate Sitemaps Dynamically
- `meta-robots` - Configure Robots for Crawl Control
- `meta-opengraph-images` - Generate Dynamic OpenGraph Images
### 8. Client Components (LOW-MEDIUM)
- `client-use-client-boundary` - Minimize 'use client' Boundary Scope
- `client-children-pattern` - Pass Server Components as Children to Client Components
- `client-hydration-mismatch` - Avoid Hydration Mismatches
- `client-third-party-scripts` - Load Third-Party Scripts Efficiently
## How to Use
Run in the project root first:
```bash
npx -y @pymodel/react-frontend-skills detect # installed vs. targeted versions
npx -y @pymodel/react-frontend-skills scan --skill nextjs # deprecated APIs → rule IDs
```
Read `references/<rule-id>.md` for each rule above that matches the task and for each `scan` hit: why it matters, then incorrect vs. correct code.
## Related Skills
- For React 19 fundamentals, see `react` skill
- For data fetching patterns, see `tanstack-query` skill
- For client-side forms, see `react-hook-form` skill