Struggling with Next.js 16 App Router? Migrate Faster & Smarter

Published: (November 29, 2025 at 05:07 PM EST)
4 min read
Source: Dev.to

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
Back to Blog

Related posts

Read more »

Bf-Trees: Breaking the Page Barrier

Hello, I'm Maneshwar. I'm working on FreeDevTools – an online, open‑source hub that consolidates dev tools, cheat codes, and TLDRs in one place, making it easy...