Struggling with Next.js 16 App Router? Migrate Faster & Smarter
Source: Dev.to
What’s Actually Changed in Next.js 16?
Next.js 16 introduces several paradigm shifts that affect how you structure your applications.
1. Async params and searchParams
In Next.js 15 and earlier, params and searchParams were synchronous objects. In Next.js 16 they are Promises that must be awaited.
Before (Next.js 15):
// app/blog/[slug]/page.tsx
export default function BlogPost({ params }: { params: { slug: string } }) {
const { slug } = params; // ✅ Synchronous access
return <h1>Post: {slug}</h1>;
}
After (Next.js 16):
// app/blog/[slug]/page.tsx
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params; // ⚠️ Now async!
return <h1>Post: {slug}</h1>;
}
Why this change? It enables better streaming and parallel data fetching. Next.js can start rendering while params are being resolved.
2. Dynamic APIs Are Now Async
Functions like cookies(), headers(), and draftMode() now return Promises.
Before:
import { cookies } from 'next/headers';
export async function getUser() {
const cookieStore = cookies(); // Sync
const token = cookieStore.get('auth-token');
return fetchUser(token);
}
After:
import { cookies } from 'next/headers';
export async function getUser() {
const cookieStore = await cookies(); // Now async
const token = cookieStore.get('auth-token');
return fetchUser(token);
}
3. Cache Components: Explicit Over Implicit
Previously, pages were cached by default, leading to “stale data” complaints. Next.js 16 flips this:
- All pages are dynamic by default (rendered per request).
- Opt into caching explicitly using the
"use cache"directive.
Example: Caching a Server Component
// app/blog/page.tsx
'use cache'; // Explicitly cache this component
export default async function BlogList() {
const posts = await fetch('https://api.example.com/posts')
.then((r) => r.json());
return (
<div>
{posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
</article>
))}
</div>
);
}
Enable cache components in your config:
// next.config.ts
const nextConfig = {
cacheComponents: true,
};
export default nextConfig;
4. Refined Caching APIs
revalidateTag() now requires a cacheLife profile for stale‑while‑revalidate behavior.
Before:
import { revalidateTag } from 'next/cache';
revalidateTag('blog-posts'); // Simple invalidation
After:
import { revalidateTag } from 'next/cache';
// Recommended: use 'max' for most use cases
revalidateTag('blog-posts', 'max');
// Built‑in profiles
revalidateTag('news-feed', 'hours');
revalidateTag('analytics', 'days');
// Custom inline profile
revalidateTag('products', { expire: 3600 });
For immediate updates (e.g., after a user action), use the new updateTag() API:
import { updateTag } from 'next/cache';
// In a Server Action
export async function createPost(formData: FormData) {
// Create post...
updateTag('blog-posts'); // Immediate cache invalidation
}
Step‑By‑Step Migration Checklist
A pragmatic approach to migrating your existing Next.js app.
Step 1: Update Dependencies
npm install next@16 react@19 react-dom@19
Or run the official codemod:
npx @next/codemod@canary upgrade latest
Step 2: Make params and searchParams Async
Search your codebase for all page components that access params or searchParams:
# Find all pages accessing params
grep -r "params:" app/
Update each component:
// Before
export default function Page({ params, searchParams }: PageProps) {
const { id } = params;
const { filter } = searchParams;
}
// After
export default async function Page({
params,
searchParams,
}: {
params: Promise<{ id: string }>;
searchParams: Promise<{ filter?: string }>;
}) {
const { id } = await params;
const { filter } = await searchParams;
}
Step 3: Update Dynamic API Calls
Add await to all cookies(), headers(), and draftMode() calls:
// Before
const cookieStore = cookies();
const headersList = headers();
// After
const cookieStore = await cookies();
const headersList = await headers();
Step 4: Migrate Middleware to Proxy
Next.js 16 renames middleware.ts to proxy.ts for clarity:
mv middleware.ts proxy.ts
Update the export:
// Before (middleware.ts)
export default function middleware(request: NextRequest) {
return NextResponse.redirect(new URL('/home', request.url));
}
// After (proxy.ts)
export default function proxy(request: NextRequest) {
return NextResponse.redirect(new URL('/home', request.url));
}
Step 5: Review Caching Strategy
Identify pages that should be cached and add the "use cache" directive:
// For static content (blog, marketing pages)
'use cache';
export default async function MarketingPage() {
// This component is now cached
}
Avoiding Common Migration Pitfalls
Pitfall 1: Mixing Sync and Async Patterns
Problem: Forgetting to await in some places while doing it correctly in others.
Solution: Enable strict TypeScript mode so the compiler catches these errors.
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true
}
}
Pitfall 2: Over‑Caching or Under‑Caching
Problem: Not understanding when to use "use cache" vs. dynamic rendering.
Rule of thumb:
- Use
"use cache"for marketing pages, blog posts, product listings, documentation. - Keep dynamic for user dashboards, real‑time data, personalized content, forms.
Pitfall 3: Ignoring Turbopack
Next.js 16 makes Turbopack the default bundler. It’s 2–5× faster for production builds. If you have a custom Webpack config, you can still opt‑in:
next dev --webpack