.cursor/plans/next.js_16_migration_92cf7fb5.plan.md
| Package | Current | Target |
| ------------ | ------- | ------ |
| next | 12.3.4 | 16.x |
| react | 17.0.2 | 19.x |
| react-dom | 17.0.2 | 19.x |
| @types/react | 17.x | 19.x |
| @next/mdx | 12.3.4 | 16.x |
graph TB
subgraph current [Current: Pages Router]
PagesDir[pages/]
PagesApp[_app.tsx]
PagesDoc[_document.tsx]
PagesMDX[docs/*.mdx]
PagesAPI[api/proxy]
end
subgraph target [Target: App Router]
AppDir[app/]
RootLayout[layout.tsx]
GlobalCSS[globals.css]
DocsGroup["(docs)/"]
RouteHandlers[api/proxy/route.ts]
end
current --> target
Update package.json:
Remove deprecated packages:
@zeit/next-source-maps (abandoned, source maps now built-in)@zeit/next-mdx (replaced by @next/mdx)enzyme, @wojtekmaj/enzyme-adapter-react-17, enzyme-to-json (no React 19 support)react-test-renderer (deprecated in React 19)Update core dependencies:
{
"next": "^16.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"@next/mdx": "^16.0.0",
"@mdx-js/react": "^3.0.0",
"@mdx-js/loader": "^3.0.0"
}
Add new dev dependencies:
{
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@testing-library/react": "^16.0.0",
"@testing-library/jest-dom": "^6.0.0"
}
Update next.config.mjs:
@zeit/next-source-maps wrapper (source maps now built-in)import createMDX from '@next/mdx';
import remarkGfm from 'remark-gfm';
const withMDX = createMDX({
extension: /\.mdx?$/,
options: {
remarkPlugins: [remarkGfm],
},
});
export default withMDX({
pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
compiler: {
styledComponents: true,
},
});
app/
├── layout.tsx # Root layout (replaces _app.tsx + _document.tsx)
├── page.tsx # Homepage (from pages/index.tsx)
├── globals.css # Global styles
├── (docs)/
│ ├── layout.tsx # Docs layout wrapper
│ ├── docs/
│ │ ├── page.tsx # /docs index
│ │ ├── basics/
│ │ │ └── page.mdx
│ │ ├── advanced/
│ │ │ └── page.mdx
│ │ ├── api/
│ │ │ └── page.mdx
│ │ ├── faqs/
│ │ │ └── page.mdx
│ │ └── tooling/
│ │ └── page.mdx
│ ├── releases/
│ │ └── page.tsx
│ ├── ecosystem/
│ │ └── page.tsx
│ └── showcase/
│ └── page.tsx
└── api/
└── proxy/
└── [asset]/
└── route.ts # Route handler (replaces API route)
Create app/layout.tsx combining _app.tsx and _document.tsx:
StyledComponentsRegistryCreate lib/registry.tsx for styled-components App Router SSR:
'use client';
import { useServerInsertedHTML } from 'next/navigation';
import { useState } from 'react';
import { ServerStyleSheet, StyleSheetManager } from 'styled-components';
export default function StyledComponentsRegistry({ children }) {
const [sheet] = useState(() => new ServerStyleSheet());
useServerInsertedHTML(() => {
const styles = sheet.getStyleElement();
sheet.instance.clearTag();
return <>{styles}</>;
});
if (typeof window !== 'undefined') return <>{children}</>;
return <StyleSheetManager sheet={sheet.instance}>{children}</StyleSheetManager>;
}
Convert pages/api/proxy/[asset].ts to Route Handler:
// app/api/proxy/[asset]/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest, { params }: { params: Promise<{ asset: string }> }) {
const { asset } = await params;
// ... handler logic
}
| Pages Router | App Router |
| -------------------- | ----------------------------- |
| getInitialProps | Server Components + fetch() |
| getStaticProps | Server Components (default) |
| getServerSideProps | cache: 'no-store' fetch |
Migrate pages/releases.tsx and pages/ecosystem.tsx:
unstable_cache or fetch with caching optionsMark interactive components with 'use client' directive:
Next.js 13+ Link no longer requires <a> child. Update components/Link.tsx if wrapping Next Link.
Replace next/router with next/navigation:
useRouter() → useRouter() from next/navigationrouter.asPath → usePathname() + useSearchParams()Replace test/setup.ts:
import '@testing-library/jest-dom';
Convert 21 test files from Enzyme/react-test-renderer to React Testing Library:
Pattern for snapshot tests:
// Before (react-test-renderer)
import renderer from 'react-test-renderer';
const tree = renderer.create(<Component />).toJSON();
expect(tree).toMatchSnapshot();
// After (React Testing Library)
import { render } from '@testing-library/react';
const { container } = render(<Component />);
expect(container).toMatchSnapshot();
Update .jest.config.js:
testEnvironment: 'jsdom'moduleNameMapper for next/navigation mockspages/ directory after migration verified| Do | Don't |
| ----------------------------------------------------------------------- | --------------------------------------------------------- |
| Run npx @next/codemod@canary upgrade latest first for automated fixes | Manually upgrade all packages at once without testing |
| Migrate one route at a time, verify functionality | Delete pages/ before app/ routes are working |
| Keep Pages and App Router running in parallel during migration | Mix next/router and next/navigation in same component |
| Mark components with state/effects as 'use client' | Add 'use client' to every component |
| Use Server Components for data fetching | Use getInitialProps in App Router |
| Test styled-components SSR works in production build | Assume dev mode behavior matches production |
yarn dev starts without errorsyarn build completes successfullyyarn test passes