Next.js 15 & React 19 Upgrade: A Complete Migration Guide

The Challenge: Upgrading a Production Next.js Application
As a solo frontend developer maintaining a complex Next.js application, I recently faced the daunting task of upgrading from Next.js 14 to 15 with React 19. This wasn’t just a routine dependency update—it involved navigating breaking changes, managing a three-server architecture, and ensuring zero downtime for a production system handling financial reconciliation data.
The BRS-client application I maintain serves as the frontend for a bank reconciliation system, with 70+ pages, complex state management, and integrations with multiple backend services. The upgrade needed to be methodical, well-tested, and production-ready.
Initial Assessment: What We Were Working With
Current Architecture
- Next.js: 14.2.29 (target: 15.3.4+)
- React: 18.3.1 (target: 19.1.0)
- TypeScript: 5.3.3 (target: 5.8.3)
- TanStack Query: v4 (target: v5)
- MUI: v5.15 (compatible with React 19)
The Good News
✅ Already using App Router (Next.js 13+ pattern)
✅ No legacy APIs (getServerSideProps
, getStaticProps
)
✅ All pages use 'use client'
(no async request API issues)
✅ No middleware.ts to update
✅ Modern font loading with next/font/local
The Challenges
⚠️ Multiple dependency compatibility issues
⚠️ TanStack Query v4 → v5 breaking changes
⚠️ React 19 defaultProps
deprecation
⚠️ Configuration updates for Next.js 15
Breaking Changes Analysis: What Could Go Wrong
Next.js 15 Major Changes
- Async Request APIs:
cookies()
,headers()
,params
,searchParams
are now async- Impact: None (project uses client components only)
- Caching Defaults: Fetch requests, GET Route Handlers, Client Router Cache now uncached by default
- Impact: Minimal (no Route Handlers, fetch usage reviewed)
- React 19 Integration: App Router uses React 19 RC
- Impact: High (dependency updates required)
React 19 Breaking Changes
defaultProps
Deprecated: Function components can’t usedefaultProps
- Impact: Medium (MUI theme configuration affected)
- Strict Effects: Enhanced strict mode behavior
- Impact: Low (strict mode currently disabled)
The Migration Strategy: 5-Phase Approach
Phase 1: Dependency Updates (4 hours)
I started with the core framework updates, using --legacy-peer-deps
to handle MUI X packages compatibility:
# Core framework updates
npm install next@^15.3.4 react@^19.1.0 react-dom@^19.1.0 --legacy-peer-deps
npm install --save-dev @types/react@^19.1.8 @types/react-dom@^19.1.6 --legacy-peer-deps
# Major dependency updates
npm install @tanstack/react-query@^5.0.0
npm install --save-dev typescript@^5.8.3 --legacy-peer-deps
Key Insight: The --legacy-peer-deps
flag was crucial for handling MUI X packages that hadn’t updated their peer dependencies yet.
Phase 2: Configuration Fixes (2 hours)
Updated next.config.js
and package.json
:
// Removed deprecated export script
// "export": "next export" - deprecated in Next.js 15
// Added TODO comments for future cleanup
// TODO: Remove ignoreBuildErrors and ignoreDuringBuilds after fixing underlying issues
Phase 3: React 19 Compatibility (3 hours)
The MUI defaultProps
Investigation
I was particularly concerned about MUI’s defaultProps
usage, but after thorough investigation:
// ✅ No defaultProps usage found in theme files
// ✅ All theme components use styleOverrides (React 19 compatible)
// ✅ Theme configuration already modern and compatible
Metadata API Migration
Migrated from manual <head>
tags to Next.js 15 Metadata API:
// Before: Manual head tags
export default function RootLayout({ children }) {
return (
<html>
<head>
<title>PM-International Workspace</title>
<meta name="description" content="..." />
</head>
<body>{children}</body>
</html>
);
}
// After: Declarative metadata
export const metadata: Metadata = {
title: 'PM-International Workspace',
description: 'Bank Reconciliation System - BRS Client',
icons: { ... }
}
export const viewport: Viewport = {
width: 'device-width',
initialScale: 1,
}
Phase 4: TanStack Query v5 Migration (Critical)
This was the most complex part of the upgrade. TanStack Query v5 introduced significant breaking changes:
Before (v4):
mutation.mutate(data, {
onSuccess: (result) => {
/* success logic */
},
onError: (error) => {
/* error logic */
},
});
After (v5):
try {
const result = await mutation.mutateAsync(data);
/* success logic */
} catch (error) {
/* error logic */
}
Files Successfully Migrated:
- ✅
src/sections/recon/management/monthly-bonus-dialog.tsx
- ✅
src/sections/permission-management/groups/group-dialog.tsx
- ✅
src/sections/auth/password-reset-dialog.tsx
- ✅
src/sections/permission-management/roles/role-dialog.tsx
- ✅
src/layouts/dashboard/vertical-layout/side-nav.test.tsx
Key Changes:
cacheTime
→gcTime
(garbage collection time)- Mutation callbacks deprecated in favor of async/await
- Enhanced error handling with try/catch patterns
Phase 5: Testing & Validation (2 hours)
Test Suite Results:
- ✅ Unit Tests: 52/54 tests passing (96% pass rate)
- ✅ React 19 Compatibility: All tests run successfully
- ✅ TypeScript Compilation: All files compile successfully
- ✅ Production Build: All 70 pages compile in 10.0s
Critical User Flow Testing:
- ✅ Authentication flow working correctly
- ✅ All 70 pages accessible and functional
- ✅ Three-server architecture functioning
- ✅ Form submissions working with new mutation patterns
The Unexpected Challenges
1. Peer Dependency Warnings
The upgrade generated numerous peer dependency warnings, but these were expected and non-blocking:
npm WARN ERESOLVE overriding peer dependency
npm WARN While resolving: @mui/x-data-grid@6.18.0
npm WARN Found: react@19.1.0
npm WARN node_modules/react
npm WARN react@"^19.1.0" from the root project
Solution: Used --legacy-peer-deps
flag and documented that these warnings are expected.
2. Test Failures in Complex Files
Two test files with extensive mocking failed after the upgrade:
// Complex test with extensive mocking
// Required updates to mock implementations for React 19
Solution: Marked as non-blocking for production, to be addressed incrementally.
3. Remaining Legacy Patterns
~50+ files still use old TanStack Query callback patterns, but the build passes:
// These still work but should be migrated incrementally
mutation.mutate(data, {
onSuccess: (result) => {
/* legacy pattern */
},
});
Solution: Incremental migration during ongoing development.
Performance Improvements Achieved
Build Performance
- Build Time: 10.0s (excellent performance)
- Static Generation: 70/70 pages (100% success)
- Bundle Optimization: Proper code splitting maintained
Runtime Benefits
- Enhanced concurrent features with React 19
- Better error boundaries and form handling
- Improved caching strategies
- Modern async/await patterns
Key Takeaways
➡️ Planning is Everything
The 5-phase approach with clear rollback strategies was crucial. Each phase was tested independently before proceeding.
➡️ Peer Dependencies Matter
The --legacy-peer-deps
flag was essential for handling MUI X packages that hadn’t updated their peer dependencies yet.
➡️ Incremental Migration Works
Not everything needs to be perfect immediately. The ~50 files with legacy patterns can be migrated incrementally.
➡️ Testing Strategy is Critical
Having a comprehensive test suite (96% pass rate) gave confidence that the upgrade didn’t break critical functionality.
➡️ Documentation Saves Time
The detailed upgrade plan served as both a roadmap and documentation for future reference.
Prevention Tips
✅ Do This
- Create a comprehensive upgrade plan with phases
- Use
--legacy-peer-deps
for complex dependency trees - Test each phase independently
- Maintain a rollback strategy
- Document all changes and decisions
❌ Don’t Do This
- Upgrade everything at once without testing
- Ignore peer dependency warnings without understanding them
- Skip the testing phase
- Forget to document the process
- Assume all breaking changes are documented
Resources
- Next.js 15 Migration Guide
- React 19 Breaking Changes
- TanStack Query v5 Migration Guide
- Next.js 15 Metadata API
This upgrade experience taught me the importance of methodical planning and incremental testing when dealing with major framework updates. The key was not just upgrading the dependencies, but understanding the impact of each change and having a clear strategy for handling the unexpected.
Now I know to always create a phased approach with clear rollback strategies when undertaking major framework upgrades, and that peer dependency management is often more complex than the actual code changes.