next, create an app/ directory with a root layout and catch-all page, move your CRA entry point into a 'use client' component, then incrementally adopt file-based routing, server components, and SSR to replace react-scripts.npm install next@latest && npx next devwindow/document in components that Next.js renders on the server — wrap browser-only code in useEffect or dynamic imports with ssr: false. Also, Next.js 16 makes all request APIs (params, searchParams, cookies, headers) fully async — synchronous access is removed.node --version before starting. [src5]output: 'export' disables useParams, API routes, proxy (formerly middleware), and all server-side features. Remove it only after confirming the deployment target supports Node.js server. [src1]params and searchParams are fully async Promises in Next.js 16 — synchronous access was removed. All page/layout components receiving params must await them. [src5]dev and build. If the project has a custom webpack config, the build will fail. Use --webpack flag to opt out, or migrate the config. [src5]app/layout.tsx (or layout files). Importing global CSS in page or component files causes a build error. [src1][[...slug]] approach during incremental migration. [src2]| CRA Pattern | Next.js Equivalent | Example |
|---|---|---|
react-scripts start | next dev | npm run dev uses Turbopack by default (Next.js 16) |
react-scripts build | next build | Outputs to .next/ (or build/ with distDir); Turbopack default in 16 |
public/index.html | app/layout.tsx (Root Layout) | Metadata API replaces manual <head> tags |
src/index.tsx (ReactDOM.render) | app/page.tsx + 'use client' wrapper | Entry point becomes a Server Component page |
react-router-dom <Route> | File-based routing (app/{route}/page.tsx) | app/dashboard/page.tsx = /dashboard |
<Link to="/path"> | <Link href="/path"> | import Link from 'next/link' |
useNavigate() / useHistory() | useRouter() from next/navigation | const router = useRouter(); router.push('/path') |
useParams() (react-router) | useParams() from next/navigation | Dynamic route: app/users/[id]/page.tsx |
REACT_APP_* env vars | NEXT_PUBLIC_* env vars | NEXT_PUBLIC_API_URL exposed to browser |
proxy in package.json | rewrites() in next.config.ts | { source: '/api/:path*', destination: 'http://backend:3001/:path*' } |
import img from './image.png' (string) | import img from './image.png' (object with .src) | Use <img src={img.src} /> or <Image src={img} /> |
CSS Modules (*.module.css) | CSS Modules (same, built-in) | No change needed |
import './global.css' in index.tsx | import './global.css' in app/layout.tsx | Global CSS must be imported in layout |
reportWebVitals() | Built-in Next.js analytics or @next/third-parties | Remove CRA’s web vitals setup |
Custom webpack in eject / craco | webpack key in next.config.ts (requires --webpack in 16) | webpack: (config) => { ... return config } |
homepage field in package.json | basePath in next.config.ts | basePath: '/my-app' |
middleware.ts (Next.js 15) | proxy.ts (Next.js 16) | Renamed: export proxy(request) instead of middleware(request) |
START
├── Is the CRA app purely client-side (SPA) with no SSR needs?
│ ├── YES → Use output: 'export' in next.config.ts for static SPA
│ └── NO ↓
├── Does the app use react-router-dom?
│ ├── YES → Phase 1: Keep react-router via catch-all route, Phase 2: Migrate to file-based routing
│ └── NO ↓
├── Does the app need server-side rendering or API routes?
│ ├── YES → Remove output: 'export', create API routes in app/api/, use Server Components
│ └── NO ↓
├── Does the app use CRACO or ejected webpack config?
│ ├── YES → Use --webpack flag in Next.js 16, map custom webpack plugins to next.config.ts
│ └── NO ↓
├── Does the app rely heavily on window/document APIs?
│ ├── YES → Wrap those components with dynamic(() => import(...), { ssr: false })
│ └── NO ↓
├── Targeting Next.js 16 specifically?
│ ├── YES → Ensure Node.js >= 20.9, await all params/searchParams, rename middleware.ts to proxy.ts
│ └── NO (Next.js 15) → Synchronous params still work with deprecation warnings
└── DEFAULT → Follow the 8-step migration, start with SPA mode, adopt features incrementally
Add Next.js alongside your existing CRA dependencies. Do not remove react-scripts yet. [src1]
npm install next@latest
Update package.json scripts:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"dev:old": "react-scripts start"
}
}
Add .next, .next/dev, and next-env.d.ts to .gitignore. (Next.js 16 uses .next/dev for development output.)
Verify: npx next --version shows the installed version (15.x+ or 16.x+).
Start with static export mode to match CRA's SPA behavior. [src1]
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
output: 'export', // Static SPA mode (matches CRA behavior)
distDir: 'build', // Match CRA's output directory
}
export default nextConfig
Note: If you have a custom webpack config and are using Next.js 16, Turbopack is the default. Add --webpack to your build script or migrate to Turbopack-compatible options. [src5]
Verify: File exists at project root alongside package.json.
Replace public/index.html with app/layout.tsx. [src1, src4]
// app/layout.tsx
import type { Metadata } from 'next'
import '../src/index.css'
export const metadata: Metadata = {
title: 'My App',
description: 'Migrated from CRA to Next.js',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<div id="root">{children}</div>
</body>
</html>
)
}
Verify: Compare with your public/index.html — all <meta> tags accounted for via the Metadata API.
Use an optional catch-all route to handle all URLs, preserving your existing react-router. [src1, src2]
// app/[[...slug]]/page.tsx
import { ClientOnly } from './client'
export function generateStaticParams() {
return [{ slug: [''] }]
}
export default function Page() {
return <ClientOnly />
}
// app/[[...slug]]/client.tsx
'use client'
import dynamic from 'next/dynamic'
const App = dynamic(() => import('../../src/App'), { ssr: false })
export function ClientOnly() {
return <App />
}
Verify: npm run dev → app loads at http://localhost:3000 with existing routing intact.
Next.js requires the NEXT_PUBLIC_ prefix for client-side environment variables. [src1]
# .env
# BEFORE
REACT_APP_API_URL=https://api.example.com
# AFTER
NEXT_PUBLIC_API_URL=https://api.example.com
Verify: grep -rn 'REACT_APP_' --include='*.ts' --include='*.tsx' returns zero results.
CRA image imports return a string URL. Next.js imports return an object with a .src property. [src1]
// BEFORE (CRA): import returns string
import logo from './logo.png'
<img src={logo} alt="Logo" />
// AFTER (Next.js): import returns object
import logo from './logo.png'
<img src={logo.src} alt="Logo" />
// OR use next/image for optimization
import Image from 'next/image'
<Image src={logo} alt="Logo" />
Verify: All images render correctly. Check Network tab for 404s.
Replace proxy in package.json with Next.js rewrites. [src1]
// next.config.ts
const nextConfig: NextConfig = {
async rewrites() {
return [
{
source: '/api/:path*',
destination: 'http://localhost:3001/api/:path*',
},
]
},
}
Note: In Next.js 16, if you need request-level middleware logic, create a proxy.ts file (renamed from middleware.ts). [src5]
Verify: API requests proxied correctly in the Network tab.
Remove CRA-specific files and dependencies. [src1, src4]
npm uninstall react-scripts
rm public/index.html
rm src/index.tsx
rm src/react-app-env.d.ts
rm src/reportWebVitals.ts
Verify: npm run build completes without errors. npm run dev starts cleanly.
Full script: typescript-next-js-migrating-a-react-router-app-to.tsx (46 lines)
// Input: CRA app with react-router-dom route definitions
// Output: Equivalent Next.js App Router file structure
// BEFORE: CRA with react-router-dom (src/App.tsx)
// <BrowserRouter>
// <Routes>
// <Route path="/" element={<Home />} />
// <Route path="/dashboard" element={<Dashboard />} />
// <Route path="/users/:id" element={<UserProfile />} />
// </Routes>
// </BrowserRouter>
// AFTER: Next.js App Router
// app/page.tsx → /
export default function Home() {
return <h1>Home Page</h1>
}
// app/dashboard/page.tsx → /dashboard
'use client'
export default function Dashboard() {
return <h1>Dashboard</h1>
}
// app/users/[id]/page.tsx → /users/:id (Next.js 16: async params)
export default async function UserProfile({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
return <h1>User {id}</h1>
}
Full script: typescript-next-js-converting-cra-navigation-patte.tsx (38 lines)
// Input: CRA components using react-router hooks
// Output: Next.js equivalents using next/navigation
'use client'
import { useRouter, useParams, usePathname, useSearchParams } from 'next/navigation'
import Link from 'next/link'
function MyComponent() {
const router = useRouter()
const params = useParams() // { id: '123' }
const pathname = usePathname() // '/users/123'
const searchParams = useSearchParams() // URLSearchParams
return (
<div>
{/* Programmatic navigation */}
<button onClick={() => router.push('/dashboard')}>Go</button>
{/* Declarative navigation (preferred) */}
<Link href="/dashboard">Dashboard</Link>
{/* Replace instead of push */}
<button onClick={() => router.replace('/login')}>Login</button>
{/* Go back */}
<button onClick={() => router.back()}>Back</button>
</div>
)
}
export default MyComponent
Full script: typescript-next-js-wrapping-browser-only-code-for-.tsx (36 lines)
// Input: CRA component using window/document/localStorage
// Output: Next.js component with SSR-safe browser API access
'use client'
import { useEffect, useState } from 'react'
import dynamic from 'next/dynamic'
// Pattern 1: useEffect guard
function ThemeToggle() {
const [theme, setTheme] = useState<'light' | 'dark'>('light')
useEffect(() => {
const saved = localStorage.getItem('theme') as 'light' | 'dark' | null
if (saved) setTheme(saved)
}, [])
const toggle = () => {
const next = theme === 'light' ? 'dark' : 'light'
setTheme(next)
localStorage.setItem('theme', next)
}
return <button onClick={toggle}>Theme: {theme}</button>
}
// Pattern 2: Dynamic import with ssr: false
const MapComponent = dynamic(
() => import('../components/Map'),
{ ssr: false, loading: () => <p>Loading map...</p> }
)
export default function LocationPage() {
return (
<div>
<ThemeToggle />
<MapComponent />
</div>
)
}
// Input: Next.js 15 page component with synchronous params
// Output: Next.js 16 page component with async params (required)
// BEFORE (Next.js 15 — synchronous, deprecated):
// export default function UserPage({ params }: { params: { id: string } }) {
// return <h1>User {params.id}</h1>
// }
// AFTER (Next.js 16 — async params required):
export default async function UserPage({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
return <h1>User {id}</h1>
}
// With searchParams (also async in Next.js 16):
export default async function SearchPage({
searchParams,
}: {
searchParams: Promise<{ q?: string }>
}) {
const { q } = await searchParams
return <h1>Results for: {q}</h1>
}
// ❌ BAD — Next.js only allows global CSS imports in layout.tsx
// app/dashboard/page.tsx
import '../../styles/global.css' // Build error
export default function Dashboard() {
return <h1>Dashboard</h1>
}
// ✅ GOOD — Global CSS imported in app/layout.tsx
import '../styles/global.css'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
// ❌ BAD — react-router-dom Link uses 'to' prop
import { Link } from 'react-router-dom'
function Nav() {
return <Link to="/about">About</Link>
}
// ✅ GOOD — next/link uses 'href' prop
import Link from 'next/link'
function Nav() {
return <Link href="/about">About</Link>
}
// ❌ BAD — Server Component runs on the server, crashes
// app/analytics/page.tsx (Server Component by default)
const url = window.location.href // ReferenceError: window is not defined
export default function Analytics() {
return <p>Current URL: {url}</p>
}
// ✅ GOOD — Client component with browser API in useEffect
'use client'
import { useState, useEffect } from 'react'
export default function Analytics() {
const [url, setUrl] = useState('')
useEffect(() => {
setUrl(window.location.href)
}, [])
return <p>Current URL: {url}</p>
}
// ❌ BAD — REACT_APP_ variables are undefined in Next.js
const apiKey = process.env.REACT_APP_API_KEY // undefined
// ✅ GOOD — NEXT_PUBLIC_ prefix works in both server and client
const apiKey = process.env.NEXT_PUBLIC_API_KEY
// ❌ BAD — Next.js image imports return an object, not a string
import logo from './logo.png'
<img src={logo} /> // Renders as [object Object]
// ✅ GOOD — Use .src for <img> or the Image component
import logo from './logo.png'
<img src={logo.src} alt="Logo" />
// Even better: Next.js Image with auto optimization
import Image from 'next/image'
<Image src={logo} alt="Logo" width={200} height={50} />
// ❌ BAD — Removing react-router and converting 50 routes in one PR
// This causes weeks of merge conflicts and blocks feature development
npm uninstall react-router-dom // Too early!
// ✅ GOOD — Catch-all route handles unmigrated pages
// app/[[...slug]]/page.tsx keeps react-router working
// Migrate one route at a time:
// 1. Create app/dashboard/page.tsx
// 2. Remove /dashboard from react-router
// 3. Test, ship, repeat
// ❌ BAD — Synchronous params access removed in Next.js 16
export default function Page({ params }: { params: { id: string } }) {
return <h1>{params.id}</h1> // TypeError in Next.js 16
}
// ✅ GOOD — Async params access required in Next.js 16
export default async function Page({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
return <h1>{id}</h1>
}
window, document, or localStorage crash during SSR. Fix: Add 'use client' directive and wrap browser API calls in useEffect, or use dynamic(() => import(...), { ssr: false }). [src1, src2]app/layout.tsx. Importing in page or component files throws a build error. Fix: Move all global CSS imports to app/layout.tsx and use CSS Modules for component-scoped styles. [src1]suppressHydrationWarning on specific elements, or render a loading placeholder server-side. [src2]REACT_APP_* variables silently become undefined because Next.js only exposes NEXT_PUBLIC_* to the client. Fix: Bulk rename with sed -i 's/REACT_APP_/NEXT_PUBLIC_/g' .env*. [src1]@svgr/webpack and add it to next.config.ts webpack config. [src2]src/pages/, Next.js may treat it as Pages Router routes. Fix: Rename to src/views/ or src/router-pages/. [src2]output: 'export' disables useParams, API routes, proxy, and server features. Fix: Remove output: 'export' once you need server-side capabilities. [src1]react-app-env.d.ts provides image import types. Fix: Ensure next-env.d.ts is in your tsconfig.json include array. [src1]webpack configs will fail. Fix: Add --webpack flag or migrate to Turbopack-compatible turbopack.resolveAlias and loaders. [src5]~ prefix for Sass node_modules imports. Fix: Remove the ~ prefix or use turbopack.resolveAlias to map ~* to *. [src5]# Verify Next.js is installed and working
npx next --version
# Check Node.js version (must be >= 20.9 for Next.js 16)
node --version
# Check for remaining CRA references
grep -rn 'react-scripts' --include='*.json' --include='*.js' --include='*.ts'
# Find remaining REACT_APP_ environment variable usage
grep -rn 'REACT_APP_' --include='*.ts' --include='*.tsx' --include='*.js' --include='*.env*'
# Count remaining react-router-dom imports (track migration progress)
grep -rn "from 'react-router" --include='*.tsx' --include='*.ts' | wc -l
# Analyze Next.js build output
npx next build 2>&1 | tail -20
# Check for SSR-unsafe window/document access in server components
grep -rn 'window\.\|document\.' --include='*.tsx' --include='*.ts' | grep -v 'node_modules' | grep -v "'use client'"
# Check for synchronous params access (needs async in Next.js 16)
grep -rn 'params\.' --include='page.tsx' --include='layout.tsx' | grep -v 'await'
# Run Next.js codemod for automated migration
npx @next/codemod@canary upgrade latest
| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| Next.js 16 (2026) | Current | Async request APIs enforced; Turbopack default; middleware → proxy; Node.js 18 dropped; next/legacy/image deprecated; next lint removed | npx @next/codemod@canary upgrade latest for automated migration |
| Next.js 15 (2025) | LTS | params is a Promise (sync still works with warnings); Turbopack stable for dev; fetch/GET no longer cached by default | const { id } = await params in page components; recommended target for stability |
| Next.js 14 (2023) | Maintenance | App Router stable, Server Actions stable | Solid target for CRA migrations |
| Next.js 13 (2022) | EOL | App Router introduced (beta) | Pages Router still supported alongside App Router |
| CRA 5.x (2021) | Deprecated (Feb 2025) | Last release, no active maintainers | Migrate to Next.js, Vite, or React Router 7 |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| App needs SSR, SSG, or ISR for SEO/performance | App is a pure client-side dashboard with no SEO needs | Vite + React Router |
| Team wants built-in routing, API routes, and proxy (middleware) | App is tiny (<10 components) and CRA still works | Stay on CRA or migrate to Vite |
| Need image/font optimization and streaming SSR | Backend team owns API and app is purely a SPA consumer | Vite (faster builds, simpler config) |
| Deploying to Vercel or need edge runtime support | Need full control over server (Express/Fastify) | Remix or custom Vite + Express |
| Enterprise app with SEO requirements and complex routing | Building a React Native app | Expo |
| Want React Server Components and React Compiler | Complex custom webpack config with no time to migrate | Vite with manual React setup |
output: 'export' in next.config.ts. Server features can be adopted incrementally by removing this flag. [src1]--webpack flag or migrate to Turbopack-compatible options. [src5]pages/ and app/ directories can coexist in Next.js for incremental migration, but the same route must not exist in both. [src6]public/ folder works similarly in Next.js, but %PUBLIC_URL% references must be removed — Next.js uses / directly. [src1]middleware.ts to proxy.ts and the middleware() export to proxy(). The edge runtime is not supported in proxy — it runs on the Node.js runtime. [src5]useEffectEvent, Activity API). The React Compiler is now stable (opt-in via reactCompiler: true). [src5]