From zenbu-powers
Next.js 16 (App Router) complete technical reference with React 19.2 and Turbopack. Covers App Router file conventions (layout, page, loading, error, not-found, route groups, parallel routes, intercepting routes), Server vs Client Components ("use client" boundary), Cache Components ("use cache" directive, cacheLife, cacheTag, updateTag, refresh), cacheComponents config, Partial Prerendering (PPR), proxy.ts (replacement for middleware.ts), data fetching (async Server Components, streaming, Suspense), Server Actions ("use server"), Route Handlers (route.ts), next.config.ts (turbopack, cacheComponents, reactCompiler, rewrites, redirects, images, output: standalone), Image optimization (next/image), Metadata API (generateMetadata, opengraph-image), caching and revalidation (revalidatePath, revalidateTag with cacheLife profile, updateTag, refresh), ISR, static vs dynamic rendering, React 19.2 features (View Transitions, useEffectEvent, Activity), React Compiler support, Turbopack (stable default bundler), and v16 breaking changes (fully async request APIs, middleware renamed to proxy, image defaults, ESLint changes, parallel routes default.js requirement). Use this skill whenever writing, reviewing, or debugging code that imports from "next", "next/image", "next/link", "next/navigation", "next/headers", "next/cache", "next/server", or "next/og", or when working with App Router file conventions, proxy.ts, next.config, Cache Components, or ISR/revalidation logic. Also use when the user mentions Next.js 16, upgrading to v16, or any v16-specific features like Cache Components or proxy.ts.
npx claudepluginhub zenbuapps/zenbu-powers --plugin zenbu-powersThis skill uses the workspace's default tool permissions.
Target version: `next ^16.2.x` with React 19.2.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
Guides code writing, review, and refactoring with Karpathy-inspired rules to avoid overcomplication, ensure simplicity, surgical changes, and verifiable success criteria.
Share bugs, ideas, or general feedback.
Target version: next ^16.2.x with React 19.2.
Official docs: https://nextjs.org/docs (App Router mode).
Released: October 21, 2025. Current patch: 16.2.4 (April 2026).
v16 Headline: Cache Components (
"use cache"+cacheComponents: true) replace the implicit caching model. All data fetching is dynamic by default unless explicitly cached. Turbopack is now the default bundler.middleware.tsis deprecated in favor ofproxy.ts.
| Topic | Where |
|---|---|
| File conventions, route groups, dynamic segments | This file, Section 1 |
| Server vs Client Components, "use client" rules | This file, Section 2 |
| Data fetching, streaming, Suspense | This file, Section 3 |
| Server Actions ("use server") | This file, Section 4 |
| Cache Components ("use cache", cacheLife, cacheTag) | This file, Section 5 |
| Caching APIs (updateTag, revalidateTag, refresh) | This file, Section 6 |
| Static vs dynamic rendering, PPR | This file, Section 7 |
| v16 breaking changes, migration from v15 | This file, Section 8 |
| proxy.ts (replaces middleware.ts), next.config.ts | references/proxy-and-config.md |
| Route Handlers, API function signatures | references/api-functions.md |
| Image, Metadata API, OG images | references/image-and-metadata.md |
| Parallel routes, intercepting routes, error handling | references/advanced-routing.md |
| React 19.2, React Compiler, Turbopack | references/react-and-tooling.md |
| Full v15->v16 migration guide, codemods, pitfalls | references/migration-from-v15.md |
| Cache Components migration (route config -> "use cache") | references/cache-components-migration.md |
| File | Purpose | Client Component? |
|---|---|---|
layout.tsx | Shared UI wrapping children; state preserved across navigations | No |
page.tsx | Unique UI for a route; makes route publicly accessible | No |
loading.tsx | Instant loading UI (auto-wrapped in <Suspense>) | No |
error.tsx | Error boundary for route segment | Yes |
not-found.tsx | 404 UI triggered by notFound() | No |
template.tsx | Like layout but re-mounts on navigation | No |
default.tsx | Fallback for parallel route slots (required in v16) | No |
route.ts | API endpoint; cannot coexist with page.tsx at same level | N/A |
global-error.tsx | Root error boundary; must include <html> and <body> | Yes |
proxy.ts | Request interceptor (replaces middleware.ts) | N/A |
// app/layout.tsx -- MUST contain <html> and <body>
export default function RootLayout({ children }: { children: React.ReactNode }) {
return <html lang="zh-TW"><body>{children}</body></html>;
}
Route groups (folder) organize without affecting URL. Each can have its own layout.
| Pattern | Example | Matches |
|---|---|---|
[slug] | app/blog/[slug]/page.tsx | /blog/hello |
[...slug] | app/shop/[...slug]/page.tsx | /shop/a, /shop/a/b/c |
[[...slug]] | app/docs/[[...slug]]/page.tsx | /docs, /docs/a/b |
v16 removes synchronous access entirely. params and searchParams are Promise types.
// page.tsx
export default async function Page({
params, searchParams,
}: {
params: Promise<{ slug: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
const { slug } = await params;
const { query } = await searchParams;
}
Type helpers via npx next typegen: PageProps<'/blog/[slug]'>, LayoutProps, RouteContext.
Client Components use React.use() to unwrap: const { id } = use(props.params);
'use client' at top of file marks a serialization boundary -- all imports become client bundle.'use client' as deep as possible (only on interactive leaves).| Server | Client |
|---|---|
| Data fetching (DB, API, secrets) | State (useState), event handlers |
| Reduce client JS | Lifecycle effects (useEffect) |
| Async component functions | Browser APIs (window, localStorage) |
<ClientModal><ServerCart /></ClientModal> (pass Server as children)import 'server-only' / import 'client-only'; only NEXT_PUBLIC_* env vars exposed to clientexport default async function Page() {
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
fetch calls are memoized within one render pass."use cache" + cacheLife to opt in.React.cache() for deduplication.const [artist, albums] = await Promise.all([getArtist(id), getAlbums(id)]);
import { Suspense } from 'react';
<Suspense fallback={<Skeleton />}>
<AsyncComponent /> {/* Streams in when ready */}
</Suspense>
loading.tsx wraps the entire page segment in <Suspense> automatically.
Server passes promise as prop (don't await), Client unwraps with use():
'use client';
import { use } from 'react';
export function PostsList({ posts }: { posts: Promise<Post[]> }) { const data = use(posts); /* render */ }
// app/actions.ts
'use server';
export async function createPost(formData: FormData) {
const session = await auth();
if (!session?.user) throw new Error('Unauthorized');
await db.post.create({ data: { title: formData.get('title') } });
revalidatePath('/posts');
redirect('/posts');
}
<form action={createPost}> (progressive enhancement, works without JS).onClick={async () => { await serverFn(); }} (Client Components only).const [state, action, pending] = useActionState(fn, init).'use server' file.updateTag('posts'); // Read-your-writes (Server Actions only)
revalidateTag('posts', 'max'); // SWR (Server Actions + Route Handlers)
refresh(); // Refresh uncached data (Server Actions only)
revalidatePath('/posts'); // Path-based invalidation
redirect('/posts'); // Redirect (call after revalidation)
Enabled by cacheComponents: true in next.config.ts. This is the v16 opt-in caching model.
Set cacheComponents: true in next.config.ts. When enabled: all data is dynamic by default,
"use cache" opts into caching at page/component/function level, PPR is automatic,
React <Activity> preserves state during navigation.
// File level -- caches all exports (all must be async)
'use cache';
export default async function Page() { /* ... */ }
// Component level
export async function MyComponent() {
'use cache';
return <></>;
}
// Function level
export async function getData() {
'use cache';
const data = await fetch('/api/data');
return data;
}
Variants:
"use cache" -- default in-memory LRU cache"use cache: remote" -- platform-provided cache handler (Redis/KV) for serverless"use cache: private" -- for compliance when runtime data cannot be refactoredCache key is auto-generated from: Build ID + Function ID + serialized arguments + closure variables.
import { cacheLife } from 'next/cache';
export default async function Page() {
'use cache';
cacheLife('days');
const posts = await getBlogPosts();
return <div>{/* render */}</div>;
}
| Profile | stale | revalidate | expire |
|---|---|---|---|
default | 5 min | 15 min | never |
seconds | 30s | 1s | 1 min |
minutes | 5 min | 1 min | 1h |
hours | 5 min | 1h | 1d |
days | 5 min | 1d | 1w |
weeks | 5 min | 1w | 30d |
max | 5 min | 30d | 1y |
Custom profiles in next.config.ts via cacheLife: { name: { stale, revalidate, expire } }.
Inline: cacheLife({ stale: 3600, revalidate: 900, expire: 86400 }).
See references/api-functions.md for full API details.
import { cacheTag } from 'next/cache';
export async function getProducts() {
'use cache';
cacheTag('products');
cacheLife('hours');
return db.query('SELECT * FROM products');
}
cacheTag('tag-one', 'tag-two')Pass dynamic content as children through cached components:
async function CachedWrapper({ children }: { children: React.ReactNode }) {
'use cache';
const data = await fetch('/api/cached');
return <div><Static data={data} />{children}</div>;
}
Server Actions only. Read-your-writes: expires cache and immediately fetches fresh data.
'use server';
import { updateTag } from 'next/cache';
export async function updateProfile(userId: string, data: Profile) {
await db.users.update(userId, data);
updateTag(`user-${userId}`); // User sees changes immediately
}
Now requires cacheLife profile as second argument for SWR behavior.
import { revalidateTag } from 'next/cache';
revalidateTag('blog-posts', 'max'); // Recommended
revalidateTag('news-feed', 'hours'); // Other profiles
revalidateTag('products', { expire: 3600 }); // Inline
// Single argument form is DEPRECATED (TypeScript error)
Server Actions only. Refreshes uncached data without touching the cache.
'use server';
import { refresh } from 'next/cache';
export async function markNotificationRead(id: string) {
await db.notifications.markAsRead(id);
refresh(); // Refreshes uncached UI (e.g., notification count)
}
| API | Context | Behavior | Use Case |
|---|---|---|---|
updateTag(tag) | Server Actions only | Immediate expiry + fresh data | Forms, user settings |
revalidateTag(tag, profile) | Server Actions + Route Handlers | Stale-while-revalidate | Blog posts, catalogs |
refresh() | Server Actions only | Re-renders uncached data | Notification counts, live metrics |
revalidatePath(path) | Server Actions + Route Handlers | Path-based invalidation | Broad cache clear |
cookies(), headers(), searchParams access (without "use cache")export const dynamic = 'force-dynamic'"use cache"generateStaticParams provides all paramsexport const dynamic = 'force-static'With cacheComponents: true, PPR is automatic: static shell from "use cache" + <Suspense>
fallbacks; dynamic holes stream uncached content at request time.
Pattern: wrap cached content with "use cache" + cacheLife, wrap dynamic in <Suspense>:
// Static (cached)
async function BlogContent({ slug }: { slug: string }) {
'use cache'; cacheLife('days');
return <article>{(await getPost(slug)).content}</article>;
}
// Dynamic (uncached) -- wrap in Suspense
async function VisitorCount({ slug }: { slug: string }) {
return <span>{await getVisitorCount(slug)} views</span>;
}
With cacheComponents enabled, React's <Activity> preserves component state during navigation.
Previous route set to mode="hidden" (not unmounted). State intact on back-navigation.
Full migration guide: references/migration-from-v15.md Cache Components migration: references/cache-components-migration.md
| Requirement | Minimum |
|---|---|
| Node.js | 20.9.0 |
| TypeScript | 5.1.0 |
| Chrome/Edge | 111+ |
| Firefox | 111+ |
| Safari | 16.4+ |
npx @next/codemod@canary upgrade latest
The upgrade command runs applicable codemods automatically. Available v16 codemods:
next-async-request-api -- migrates cookies/headers/params/searchParams to asyncmiddleware-to-proxy -- renames middleware.ts to proxy.ts, updates exports and config flagsnext-lint-to-eslint-cli -- migrates next lint to ESLint CLI with flat configremove-experimental-ppr -- removes experimental_ppr route segment configremove-unstable-prefix -- removes unstable_ prefix from cacheLife/cacheTagFor individual codemod commands and before/after examples, see references/migration-from-v15.md Section 2.
Synchronous access removed entirely. Must await cookies(), headers(), draftMode(),
params, searchParams. Image/sitemap params (opengraph-image, twitter-image, icon,
apple-icon, sitemap) also receive params and id as Promises.
See references/api-functions.md and references/migration-from-v15.md Section 3.1.
Rename file and function. Config: skipMiddlewareUrlNormalize -> skipProxyUrlNormalize.
Edge runtime NOT supported in proxy.ts (Node.js only).
See references/proxy-and-config.md.
Turbopack is now the default bundler. Custom webpack configs cause build failure unless:
next build --webpack to opt outnext build --turbopack to ignore webpack configturbopack optionsTurbopack config moved from experimental.turbopack to top-level turbopack.
Sass tilde imports (~) not supported; use resolveAlias workaround.
See references/migration-from-v15.md Section 3.3.
All parallel route slots must have explicit default.tsx. Builds fail without them.
// app/@modal/default.tsx
export default function Default() {
return null;
}
AMP support, next lint, serverRuntimeConfig/publicRuntimeConfig, experimental.dynamicIO
(use cacheComponents), experimental.ppr (use cacheComponents), experimental_ppr route
export, auto scroll-behavior: smooth override (use data-scroll-behavior="smooth" on <html>),
unstable_rootParams(), devIndicators options, build size/First Load JS metrics.
minimumCacheTTL: 60s -> 4h. imageSizes: 16 removed. qualities: [1..100] -> [75].
dangerouslyAllowLocalIP: blocked by default. maximumRedirects: unlimited -> 3.
Local images with query strings require images.localPatterns.search config.
See references/image-and-metadata.md for details.
Now requires second argument: revalidateTag('posts', 'max'). Single-arg deprecated.
To migrate from route segment configs (dynamic, revalidate, fetchCache) to Cache Components
("use cache" + cacheLife + cacheTag), see references/cache-components-migration.md.
Key: cacheComponents: true is opt-in. Edge runtime incompatible. Gradual adoption supported.
For deeper API details, read the relevant reference file: