Loading...
Loading...
Build Next.js 16 apps with App Router, Server Components/Actions, Cache Components ("use cache"), and async route params. Includes proxy.ts (replaces middleware.ts) and React 19.2. Use when: building Next.js 16 projects, or troubleshooting async params (Promise types), "use cache" directives, parallel route 404s (missing default.js), or proxy.ts CORS.
npx skill4agent add ovachiever/droid-tings nextjs"use cache"revalidateTag()updateTag()refresh()paramssearchParamscookies()headers()useEffectEvent()cloudflare-nextjsclerk-authbetter-authcloudflare-d1drizzle-orm-d1tailwind-v4-shadcnzustand-state-managementtanstack-queryreact-hook-form-zodparamssearchParamscookies()headers()draftMode()// ❌ This no longer works in Next.js 16
export default function Page({ params, searchParams }: {
params: { slug: string }
searchParams: { query: string }
}) {
const slug = params.slug // ❌ Error: params is a Promise
const query = searchParams.query // ❌ Error: searchParams is a Promise
return <div>{slug}</div>
}// ✅ Correct: await params and searchParams
export default async function Page({ params, searchParams }: {
params: Promise<{ slug: string }>
searchParams: Promise<{ query: string }>
}) {
const { slug } = await params // ✅ Await the promise
const { query } = await searchParams // ✅ Await the promise
return <div>{slug}</div>
}paramssearchParamscookies()next/headersheaders()next/headersdraftMode()next/headers// ❌ Before
import { cookies, headers } from 'next/headers'
export function MyComponent() {
const cookieStore = cookies() // ❌ Sync access
const headersList = headers() // ❌ Sync access
}
// ✅ After
import { cookies, headers } from 'next/headers'
export async function MyComponent() {
const cookieStore = await cookies() // ✅ Async access
const headersList = await headers() // ✅ Async access
}npx @next/codemod@canary upgrade latesttemplates/app-router-async-params.tsxmiddleware.tsproxy.tsproxy.tsmiddleware.tsproxy.tsmiddlewareproxymatcherconfig.matcher// middleware.ts ❌ Deprecated in Next.js 16
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const response = NextResponse.next()
response.headers.set('x-custom-header', 'value')
return response
}
export const config = {
matcher: '/api/:path*',
}// proxy.ts ✅ New in Next.js 16
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
const response = NextResponse.next()
response.headers.set('x-custom-header', 'value')
return response
}
export const config = {
matcher: '/api/:path*',
}middleware.tsproxy.tstemplates/proxy-migration.tsreferences/proxy-vs-middleware.mddefault.jsdefault.jsapp/
├── @auth/
│ ├── login/
│ │ └── page.tsx
│ └── default.tsx ← REQUIRED in Next.js 16
├── @dashboard/
│ ├── overview/
│ │ └── page.tsx
│ └── default.tsx ← REQUIRED in Next.js 16
└── layout.tsx// app/layout.tsx
export default function Layout({
children,
auth,
dashboard,
}: {
children: React.ReactNode
auth: React.ReactNode
dashboard: React.ReactNode
}) {
return (
<html>
<body>
{auth}
{dashboard}
{children}
</body>
</html>
)
}// app/@auth/default.tsx
export default function AuthDefault() {
return null // or <Skeleton /> or redirect
}
// app/@dashboard/default.tsx
export default function DashboardDefault() {
return null
}default.jstemplates/parallel-routes-with-default.tsxnext lintserverRuntimeConfigpublicRuntimeConfigexperimental.ppr"use cache"scroll-behavior: smoothnpx eslint .npx biome lint .serverRuntimeConfigprocess.env.VARIABLEexperimental.ppr"use cache"node --version # Should be 20.9+
npm --version # Should be 10+
npx next --version # Should be 16.0.0+# Using nvm
nvm install 20
nvm use 20
nvm alias default 20
# Using Homebrew (macOS)
brew install node@20
# Using apt (Ubuntu/Debian)
sudo apt update
sudo apt install nodejs npmnext/image| Setting | Next.js 15 | Next.js 16 |
|---|---|---|
| TTL (cache duration) | 60 seconds | 4 hours |
| imageSizes | | |
| qualities | | |
// next.config.ts
import type { NextConfig } from 'next'
const config: NextConfig = {
images: {
minimumCacheTTL: 60, // Revert to 60 seconds
deviceSizes: [640, 750, 828, 1080, 1200, 1920], // Add larger sizes
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], // Restore old sizes
formats: ['image/webp'], // Default
},
}
export default configtemplates/image-optimization.tsx"use cache""use cache""use cache""use cache"// app/components/expensive-component.tsx
'use cache'
export async function ExpensiveComponent() {
const data = await fetch('https://api.example.com/data')
const json = await data.json()
return (
<div>
<h1>{json.title}</h1>
<p>{json.description}</p>
</div>
)
}// lib/data.ts
'use cache'
export async function getExpensiveData(id: string) {
const response = await fetch(`https://api.example.com/items/${id}`)
return response.json()
}
// Usage in component
import { getExpensiveData } from '@/lib/data'
export async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const product = await getExpensiveData(id) // Cached
return <div>{product.name}</div>
}// app/blog/[slug]/page.tsx
'use cache'
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
return posts.map((post: { slug: string }) => ({ slug: post.slug }))
}
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const post = await fetch(`https://api.example.com/posts/${slug}`).then(r => r.json())
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
)
}templates/cache-component-use-cache.tsx// app/dashboard/page.tsx
// Static header (cached)
'use cache'
async function StaticHeader() {
return <header>My App</header>
}
// Dynamic user info (not cached)
async function DynamicUserInfo() {
const cookieStore = await cookies()
const userId = cookieStore.get('userId')?.value
const user = await fetch(`/api/users/${userId}`).then(r => r.json())
return <div>Welcome, {user.name}</div>
}
// Page combines both
export default function Dashboard() {
return (
<div>
<StaticHeader /> {/* Cached */}
<DynamicUserInfo /> {/* Dynamic */}
</div>
)
}references/cache-components-guide.mdrevalidateTag()revalidateTag()cacheLifeimport { revalidateTag } from 'next/cache'
export async function updatePost(id: string) {
await fetch(`/api/posts/${id}`, { method: 'PATCH' })
revalidateTag('posts') // ❌ Only one argument in Next.js 15
}import { revalidateTag } from 'next/cache'
export async function updatePost(id: string) {
await fetch(`/api/posts/${id}`, { method: 'PATCH' })
revalidateTag('posts', 'max') // ✅ Second argument required in Next.js 16
}'max''hours''days''weeks''default'revalidateTag('posts', {
stale: 3600, // Stale after 1 hour (seconds)
revalidate: 86400, // Revalidate every 24 hours (seconds)
expire: false, // Never expire (optional)
})'use server'
import { revalidateTag } from 'next/cache'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
await fetch('/api/posts', {
method: 'POST',
body: JSON.stringify({ title, content }),
})
revalidateTag('posts', 'max') // ✅ Revalidate with max staleness
}templates/revalidate-tag-cache-life.tsupdateTag()updateTag()revalidateTag()revalidateTag()updateTag()'use server'
import { updateTag } from 'next/cache'
export async function updateUserProfile(formData: FormData) {
const name = formData.get('name') as string
const email = formData.get('email') as string
// Update database
await db.users.update({ name, email })
// Immediately refresh cache (read-your-writes)
updateTag('user-profile')
// User sees updated data immediately (no stale data)
}updateTag()revalidateTag()templates/server-action-update-tag.tsrefresh()refresh()router.refresh()router.refresh()'use server'
import { refresh } from 'next/cache'
export async function refreshDashboard() {
// Refresh uncached data (e.g., real-time metrics)
refresh()
// Cached data (e.g., static header) remains cached
}revalidateTag()updateTag()refresh()revalidateTag()updateTag()references/cache-components-guide.mdparamsheaders()// app/api/posts/[id]/route.ts
import { NextResponse } from 'next/server'
import { headers } from 'next/headers'
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params // ✅ Await params in Next.js 16
const headersList = await headers() // ✅ Await headers in Next.js 16
const post = await db.posts.findUnique({ where: { id } })
return NextResponse.json(post)
}templates/route-handler-api.tsproxy.tsmiddleware.tsmiddleware.tsproxy.tsproxy.ts// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// Check auth
const token = request.cookies.get('token')
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: '/dashboard/:path*',
}// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
// Check auth
const token = request.cookies.get('token')
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: '/dashboard/:path*',
}templates/proxy-migration.tsreferences/proxy-vs-middleware.mddefault.jsapp/
├── @modal/
│ ├── login/page.tsx
│ └── default.tsx ← REQUIRED in Next.js 16
├── @feed/
│ ├── trending/page.tsx
│ └── default.tsx ← REQUIRED in Next.js 16
└── layout.tsx// app/@modal/default.tsx
export default function ModalDefault() {
return null // or <Skeleton /> or redirect
}default.jstemplates/parallel-routes-with-default.tsx'use client'
import { useRouter } from 'next/navigation'
import { startTransition } from 'react'
export function NavigationLink({ href, children }: { href: string; children: React.ReactNode }) {
const router = useRouter()
function handleClick(e: React.MouseEvent) {
e.preventDefault()
// Wrap navigation in startTransition for View Transitions
startTransition(() => {
router.push(href)
})
}
return <a href={href} onClick={handleClick}>{children}</a>
}/* app/globals.css */
@view-transition {
navigation: auto;
}
/* Animate elements with view-transition-name */
.page-title {
view-transition-name: page-title;
}templates/view-transitions-react-19.tsxuseEffectEvent()useEffect'use client'
import { useEffect, experimental_useEffectEvent as useEffectEvent } from 'react'
export function ChatRoom({ roomId }: { roomId: string }) {
const onConnected = useEffectEvent(() => {
console.log('Connected to room:', roomId)
})
useEffect(() => {
const connection = connectToRoom(roomId)
onConnected() // Non-reactive callback
return () => connection.disconnect()
}, [roomId]) // Only re-run when roomId changes
return <div>Chat Room {roomId}</div>
}useEffectuseMemouseCallbackimport type { NextConfig } from 'next'
const config: NextConfig = {
experimental: {
reactCompiler: true,
},
}
export default confignpm install babel-plugin-react-compiler'use client'
export function ExpensiveList({ items }: { items: string[] }) {
// React Compiler automatically memoizes this
const filteredItems = items.filter(item => item.length > 3)
return (
<ul>
{filteredItems.map(item => (
<li key={item}>{item}</li>
))}
</ul>
)
}references/react-19-integration.mdnpm run build -- --webpack// next.config.ts
import type { NextConfig } from 'next'
const config: NextConfig = {
experimental: {
turbopack: {
fileSystemCaching: true, // Beta: Persist cache between runs
},
},
}
export default configparamsType 'Promise<{ id: string }>' is not assignable to type '{ id: string }'paramsparams// ❌ Before
export default function Page({ params }: { params: { id: string } }) {
const id = params.id
}
// ✅ After
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
}searchParamsProperty 'query' does not exist on type 'Promise<{ query: string }>'searchParams// ❌ Before
export default function Page({ searchParams }: { searchParams: { query: string } }) {
const query = searchParams.query
}
// ✅ After
export default async function Page({ searchParams }: { searchParams: Promise<{ query: string }> }) {
const { query } = await searchParams
}cookies()'cookies' implicitly has return type 'any'cookies()// ❌ Before
import { cookies } from 'next/headers'
export function MyComponent() {
const cookieStore = cookies()
}
// ✅ After
import { cookies } from 'next/headers'
export async function MyComponent() {
const cookieStore = await cookies()
}default.jsError: Parallel route @modal/login was matched but no default.js was founddefault.jsdefault.tsx// app/@modal/default.tsx
export default function ModalDefault() {
return null
}revalidateTag()Expected 2 arguments, but got 1revalidateTag()cacheLife// ❌ Before
revalidateTag('posts')
// ✅ After
revalidateTag('posts', 'max')You're importing a component that needs useState. It only works in a Client Component'use client'// ✅ Add 'use client' at the top
'use client'
import { useState } from 'react'
export function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}middleware.tsWarning: middleware.ts is deprecated. Use proxy.ts instead.proxy.ts// Rename: middleware.ts → proxy.ts
// Rename function: middleware → proxy
export function proxy(request: NextRequest) {
// Same logic
}Error: Failed to compile with Turbopacknpm run build -- --webpacknext/imageInvalid src prop (https://example.com/image.jpg) on `next/image`. Hostname "example.com" is not configured under images in your `next.config.js`next.config.tsconst config: NextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'example.com',
},
],
},
}You're importing a Server Component into a Client Component// ❌ Wrong
'use client'
import { ServerComponent } from './server-component' // Error
export function ClientComponent() {
return <ServerComponent />
}
// ✅ Correct
'use client'
export function ClientComponent({ children }: { children: React.ReactNode }) {
return <div>{children}</div>
}
// Usage
<ClientComponent>
<ServerComponent /> {/* Pass as children */}
</ClientComponent>generateStaticParamsgenerateStaticParamsexport const dynamic = 'force-static'export const dynamic = 'force-static'
export async function generateStaticParams() {
const posts = await fetch('/api/posts').then(r => r.json())
return posts.map((post: { id: string }) => ({ id: post.id }))
}fetch()"use cache""use cache"'use cache'
export async function getPosts() {
const response = await fetch('/api/posts')
return response.json()
}Error: Conflicting routes: /about and /(marketing)/aboutapp/
├── (marketing)/about/page.tsx → /about
└── (shop)/about/page.tsx → ERROR: Duplicate /about
# Fix: Use different routes
app/
├── (marketing)/about/page.tsx → /about
└── (shop)/store-info/page.tsx → /store-infogenerateMetadata()generateMetadata()export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
const { id } = await params
const post = await fetch(`/api/posts/${id}`).then(r => r.json())
return {
title: post.title,
description: post.excerpt,
}
}next/font<html><body>import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'], variable: '--font-inter' })
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html className={inter.variable}> {/* ✅ Apply variable */}
<body>{children}</body>
</html>
)
}NEXT_PUBLIC_# .env
SECRET_KEY=abc123 # Server-only
NEXT_PUBLIC_API_URL=https://api # Available in browser// Server Component (both work)
const secret = process.env.SECRET_KEY
const apiUrl = process.env.NEXT_PUBLIC_API_URL
// Client Component (only public vars work)
const apiUrl = process.env.NEXT_PUBLIC_API_URLError: Could not find Server Action'use server''use server'// ❌ Before
export async function createPost(formData: FormData) {
await db.posts.create({ ... })
}
// ✅ After
'use server'
export async function createPost(formData: FormData) {
await db.posts.create({ ... })
}baseUrlpathstsconfig.json{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"],
"@/components/*": ["./app/components/*"]
}
}
}references/top-errors.mdtemplates/app-router-async-params.tsxparallel-routes-with-default.tsxcache-component-use-cache.tsx"use cache"revalidate-tag-cache-life.tsrevalidateTag()server-action-update-tag.tsupdateTag()proxy-migration.tsview-transitions-react-19.tsxnext.config.tsreferences/next-16-migration-guide.mdcache-components-guide.mdproxy-vs-middleware.mdasync-route-params.mdreact-19-integration.mdtop-errors.md/websites/nextjs| Package | Minimum Version | Recommended |
|---|---|---|
| Next.js | 16.0.0 | 16.0.0+ |
| React | 19.2.0 | 19.2.0+ |
| Node.js | 20.9.0 | 20.9.0+ |
| TypeScript | 5.1.0 | 5.7.0+ |
| Turbopack | (built-in) | Stable |
./scripts/check-versions.shcd skills/nextjs
./scripts/check-versions.sh