React is a library, not a framework. It handles the view layer well, but leaves everything else to you: routing, data fetching, server rendering, code splitting, image optimization, SEO metadata, and deployment configuration. Over time, most React projects accumulate a bespoke stack of libraries to fill these gaps — react-router, react-helmet, custom webpack configs, manual lazy loading — each one adding maintenance overhead and one more thing that can break.
The most damaging gap for most products is server-side rendering. A plain Create React App (or Vite) setup ships an empty HTML shell to the browser, then hydrates it with JavaScript. Search engines and social link previews receive that empty shell. Google has improved its ability to crawl JavaScript-rendered content, but it still treats server-rendered HTML with higher confidence and indexes it faster. If your competitors' pages are server-rendered and yours are not, they have a structural SEO advantage that no amount of content optimization can fully overcome.
The second problem is Core Web Vitals. Google's page experience signals weigh metrics like Largest Contentful Paint (LCP) and Interaction to Next Paint (INP). A React SPA that fetches data client-side after hydration will almost always underperform a server-rendered equivalent — the user stares at a blank or skeleton screen while the browser waits for JavaScript to load, parse, execute, and then make a network request to an API. That is a lot of latency stacked before the user sees anything meaningful.
For a business, these translate to: faster pages, better Google rankings, lower hosting costs (statically generated pages are cheap to serve), and a codebase that follows a convention new engineers can onboard to quickly.
A React-to-Next.js migration is usually a restructure, not a rewrite. Your component code, state management, and business logic stay largely intact. What changes is the outer shell: the routing model, where data fetching happens, and the project entry point.
Routing library and route structure (react-router v5 vs v6 matters)
Data fetching patterns (useEffect, Redux Thunk, React Query, SWR)
Any webpack plugins or CRA customizations that need a Next.js equivalent
Environment variables and which ones are consumed client-side vs server-side
Authentication patterns — especially whether you use cookies or localStorage
This audit takes a few hours for a small app and a few days for a large one. Do not skip it. The surprises discovered in audit are much cheaper to handle than the ones discovered mid-migration.
Translate each React Router route to a corresponding file. This is mostly mechanical work — often the most time-consuming part of the migration depending on how many routes your app has.
This is the biggest conceptual shift. In React SPAs, data is fetched inside useEffect or with a client-side library. In Next.js App Router, you fetch data in Server Components — async functions that run on the server, with direct access to your database or API:
// Before: client-side fetch in a React SPAfunctionBlogPage() {
const [posts, setPosts] = useState([])
useEffect(() => {
fetch('/api/posts').then(r => r.json()).then(setPosts)
}, [])
return<PostListposts={posts} />
}
// After: Server Component in Next.jsexportdefaultasyncfunctionBlogPage() {
const posts = await db.query('SELECT * FROM posts WHERE published = true')
return<PostListposts={posts} />
}
The result: no loading spinners on first render, no empty HTML shell for crawlers, no extra browser-to-API round trip on page load.
Step 5: Replace react-helmet with Next.js Metadata API#
Some things only run in the browser: localStorage, window events, third-party browser SDKs. In Next.js, these live in Client Components, marked with 'use client' at the top of the file. The default is server; add 'use client' only when the component needs interactivity or browser APIs.
Cookie-based auth needs server-aware handling. If you use httpOnly cookies for authentication, you need to read them in Server Components via Next.js's cookies() function. This trips up almost every migration.
Some npm packages do not support server environments. Libraries that reference window or document at import time will crash inside a Server Component. The fix is dynamic(() => import('./MyComponent'), { ssr: false }).
Environment variables work differently. Only variables prefixed with NEXT_PUBLIC_ are exposed to the browser bundle. Audit your .env file before cutting over — leaking secrets to the client is a real risk in this step.
Programmatic navigation must be in a Client Component.useRouter().push() from next/navigation only works in 'use client' components.
For most businesses, the return on a Next.js migration comes from three places:
SEO. Server-rendered pages are indexed faster and more reliably. Organic traffic gains are commonly observed within 60–90 days of migrating, particularly for content-heavy pages.
Conversion. Faster LCP means lower bounce rates. A 100ms improvement in load time correlates with measurable conversion improvements — the principle is well-established even if the exact number varies by industry.
Developer velocity. Next.js conventions eliminate the decision overhead of a bespoke React stack. New engineers contribute faster because the patterns are standardized and well-documented. Less time configuring, more time building.
The migration cost in developer hours is typically recovered within 6–12 months for a product with meaningful traffic and an active development team.
If you are weighing whether this makes sense for your specific app — the stack, the timeline, the cost — feel free to reach out. I am happy to talk through it.
Why Your React App Needs to Migrate to Next.js (And How to Do It) | Wonsuk Choi