Surviving the Next.js 15 Upgrade: A Small Team's Battle-Tested Migration Strategy

By amillionmonkeys
#Next.js#React#Web Development#Migration

Real migration issues we hit upgrading to Next.js 15 and React 19, including async request APIs, caching bugs, and third-party library compatibility problems.

Last month, we migrated three client sites to Next.js 15. Two went smoothly. The third broke spectacularly at 11pm on deployment night.

The problem wasn't what the official docs warned us about. It was the cascade of third-party library incompatibilities with React 19, the subtle caching behaviour changes that only surfaced in production, and the async request API changes that seemed simple in theory but wreaked havoc in practice.

In this post, I'll walk through the specific issues we encountered, how we fixed them, and provide a battle-tested migration checklist so you don't make the same mistakes.

TL;DR

Migrating to Next.js 15 takes 1-3 days depending on complexity. Main issues:

  • Async params require manual fixes beyond the codemod
  • Fetch caching defaults changed—add explicit caching
  • React 19 breaks some third-party libraries (check first)
  • Budget 2x the time you think you'll need

Skip to migration checklist

Why Upgrade to Next.js 15?

Before diving into the problems, it's worth asking: why bother upgrading at all?

Next.js 15 brings some genuinely useful improvements:

  • React 19 support - Access to Actions, useOptimistic, and improved hooks
  • Async Request APIs - Cleaner server-side data access (though this causes breaking changes)
  • Improved caching defaults - More predictable behaviour, less "magic"
  • Turbopack improvements - Faster local development (though still stabilising)
  • Better error messages - Legitimately helpful debugging information

For most projects, these improvements are worth the migration effort. But timing matters. If you're running a stable production app with tight deadlines, you might want to wait until your third-party dependencies catch up.

Next.js 15 Breaking Changes: What Broke in Production

1. Async Request APIs Broke Page Layouts

The biggest breaking change in Next.js 15 is that cookies(), headers(), and params are now async. Simple in theory. Painful in practice.

Before (Next.js 14):

export default function ProductPage({ params }) {
  const { id } = params;
  return <ProductDetails productId={id} />;
}

After (Next.js 15):

export default async function ProductPage({ params }) {
  const { id } = await params;
  return <ProductDetails productId={id} />;
}

Seems straightforward, right? The problem emerged in complex layouts where we were destructuring params in multiple places—parent layouts, nested components, and utility functions. We had to track down every single usage and make them async.

The error message we saw:

Error: Route /products/[id] used params.id.
params should be awaited before accessing its properties.

This appeared inconsistently during builds, which made debugging frustrating. The official Next.js codemod (npx @next/codemod@canary upgrade latest) caught about 70% of cases, but we still had to manually fix deeply nested components.

2. Caching Semantics Changed (And Broke Our Product Listings)

Next.js 15 changed the default caching behaviour for fetch() requests. Previously, fetch() was cached by default. Now it's not.

For one client's e-commerce site, this meant product listings were suddenly hitting the API on every single request instead of being cached. Performance dropped from 1.2s to 5.8s page loads. Our Vercel function invocations tripled, adding £45/month in hosting costs.

The fix:

// Old behaviour (cached by default in Next.js 14)
const products = await fetch('https://api.example.com/products');
 
// New behaviour in Next.js 15 (explicitly cache)
const products = await fetch('https://api.example.com/products', {
  next: { revalidate: 3600 } // Cache for 1 hour
});

We had to audit every fetch() call across all three projects and explicitly define caching strategies. This took longer than expected because some calls were buried in utility functions and third-party wrappers.

3. Third-Party Library Compatibility Nightmare

This was the late-night deployment disaster.

We use Radix UI extensively for accessible components. When we upgraded to Next.js 15 (which requires React 19), several Radix components threw errors:

Error: useEffectEvent is not exported from 'react'

The problem? React 19 renamed some experimental hooks, and Radix UI v1.0.x hadn't updated yet. We discovered this in production because our build passed locally (we were using an older Radix version in our lock file), but failed when deployed to Vercel with a fresh install. Upgrading to @radix-ui/[email protected] and other Radix packages to their latest versions fixed the compatibility issues.

Our workaround:

  • Check each library's compatibility with React 19 before upgrading
  • Pin dependencies in package.json rather than using version ranges
  • Test with a completely fresh npm install before deploying

We also hit issues with:

  • Ant Design - Required a major version upgrade
  • React Hook Form - Needed patches for React 19 compatibility
  • Custom analytics tracking - Used patterns that broke with Server Components

Tip: If you're managing multiple dependencies and worried about compatibility, we can audit your stack before migration. Get in touch.

4. Server Components Boundary Issues

Next.js 15 enforces stricter Server Component boundaries. We ran into this with event handlers passed as props:

// This worked in Next.js 14, breaks in Next.js 15
export default function ParentComponent() {
  const handleClick = () => console.log('Clicked');
  return <ChildServerComponent onClick={handleClick} />; // Error!
}

The error:

Error: Functions cannot be passed directly to Client Components
unless you explicitly expose it by marking it with "use server".

The fix required restructuring our component hierarchy—moving client-side logic into Client Components and keeping Server Components purely for data fetching.

Our Step-by-Step Migration Strategy

After three migrations, here's what works:

Phase 1: Audit & Prepare (2-3 hours)

  1. Check dependency compatibility

    • Run npm outdated to see what needs updating
    • Check each major dependency's GitHub issues for React 19 compatibility
    • Search for "[package-name] React 19" to find known issues
  2. Run the codemod

    npx @next/codemod@canary upgrade latest

    This handles most async param conversions, but don't trust it blindly.

  3. Review your caching strategy

    • Audit all fetch() calls
    • Document which data should be cached and for how long
    • Identify any custom caching logic that might break

Phase 2: Migrate Dependencies (1-2 hours)

  1. Update Next.js and React

    npm install next@latest react@latest react-dom@latest
  2. Update conflicting dependencies

    • Update UI libraries (Radix, Material-UI, etc.)
    • Update form libraries
    • Update any packages with peer dependency warnings
  3. Test build

    npm run build

    Fix any TypeScript errors or build failures before moving on.

Phase 3: Fix Breaking Changes (4-8 hours)

  1. Handle async params systematically

    • Search for all params usage: grep -r "params." .
    • Make containing functions async
    • Add await before accessing properties
  2. Add explicit caching to fetch calls

    • Find all fetch calls: grep -r "fetch(" .
    • Add caching options based on your data freshness requirements
    • Test that cached data updates correctly
  3. Fix Server/Client Component boundaries

    • Add 'use client' to components with hooks or event handlers
    • Move server-only logic to Server Components
    • Ensure props passed between boundaries are serializable

Phase 4: Test Everything (2-4 hours)

  1. Fresh install test

    rm -rf node_modules package-lock.json
    npm install
    npm run build
  2. Development testing

    • Test all major user flows
    • Check server-side rendering still works
    • Verify caching behaviour with network throttling
  3. Production preview

    • Deploy to a staging environment
    • Run through the entire application
    • Check for console errors and warnings

Should You Upgrade Now?

Honest assessment from a small team perspective:

Upgrade now if:

  • You're starting a new project (no migration headaches)
  • You need React 19 features like Actions or useOptimistic
  • You're already on Next.js 14 and have good test coverage
  • Your dependencies are actively maintained and React 19 compatible

Wait a few months if:

  • You rely heavily on third-party UI libraries that haven't updated
  • You're mid-project with tight deadlines
  • You don't have time for thorough testing
  • Your app is stable and you don't need new features

For us, the React 19 features were worth the pain. But we're a small team with the flexibility to dedicate a few days to migrations. If you're managing a critical production app with a larger team, coordinate carefully and budget more time than you think you'll need.

Migration Checklist

Use this before, during, and after your migration:

Before Migration:

  • Check all dependencies for React 19 compatibility
  • Review Next.js 15 breaking changes in official docs
  • Create a git branch for the migration
  • Document current caching behaviour
  • Set aside 2-3 days for migration + testing

During Migration:

  • Update Next.js, React, and React DOM
  • Run official codemod
  • Update incompatible dependencies
  • Convert all params access to async/await
  • Add explicit caching to fetch calls
  • Fix Server/Client Component boundaries
  • Update TypeScript types if needed
  • Test build with fresh install

After Migration:

  • Test all critical user flows
  • Check server-side rendering
  • Verify caching works as expected
  • Monitor production errors for 48 hours
  • Document any custom fixes for team reference
  • Update deployment/CI pipelines if needed

Key Takeaways

Migrating to Next.js 15 is mostly straightforward, but watch out for:

  • Async request APIs - The codemod helps, but you'll still need manual fixes in nested components
  • Caching changes - Default behaviour changed; add explicit caching to avoid performance regressions
  • Third-party dependencies - React 19 compatibility varies wildly; check before upgrading
  • Server Component boundaries - Stricter enforcement requires more careful component architecture
  • Testing is critical - Do a fresh install and test in production-like conditions before deploying

The upgrade is worth it for the React 19 features and improved developer experience, but budget more time than the official docs suggest. For our three projects, "simple" migrations took 1 day, while complex ones took 3 days including testing.


Planning a Next.js upgrade for your production app? We've migrated multiple client projects and can help you navigate the breaking changes with confidence. Let's talk about your upgrade strategy.

We also offer bespoke web development using Next.js and modern React patterns.

T: 07512 944360 | E: [email protected]

© 2025 amillionmonkeys ltd. All rights reserved.