Skip to content
Create React AppNext.js
Migration Guide

Create React App to Next.js Migration

CRA is deprecated and unmaintained; Next.js is where its ecosystem went. The migration is mostly mechanical — the two real decisions are routing and rendering strategy.

What Changes
  1. react-scripts and its webpack config disappear entirely
  2. React Router routes become file-based App Router routes
  3. index.html + react-helmet become layout.tsx + the Metadata API
  4. REACT_APP_ env vars become NEXT_PUBLIC_ (client) or plain server vars
  5. Every component is a Server Component until it opts into "use client"
Migration Path

Don't chase a full SSR rewrite on day one. Move the app into Next.js as client components first — a working port with 'use client' at the top of interactive trees — then peel components back to server components where it actually buys something (data fetching, SEO pages, bundle size). Incremental correctness beats architectural purity.

In detail

Why this migration is overdue

Create React App was officially deprecated in early 2025, and the React team's own documentation now points new projects at frameworks — Next.js first among them. That matters practically, not just politically: react-scripts pins old webpack and Babel versions, new React features land framework-first, and every security advisory in CRA's frozen dependency tree is now yours to patch around. Staying on CRA means maintaining a build system its own authors have abandoned.

What you gain beyond survival: server rendering for pages that need SEO, file-based routing that removes a layer of configuration, image and font optimization built in, and API routes that eliminate the separate Express server many CRA apps carry just to hide an API key. This portfolio runs on Next.js App Router with a few hundred statically generated pages — the pSEO architecture it uses simply isn't buildable on CRA, where every page is an empty div until JavaScript loads. If organic search matters to your app at all, that last point is the whole argument.

Routing: the only genuinely fiddly part

React Router's route config translates to the App Router's filesystem: /products/:id becomes app/products/[id]/page.tsx, nested routes with an <Outlet /> become nested layout.tsx files, and index routes become page.tsx at the folder root. The mechanical mapping is quick; the fiddly parts are the idioms around it. useNavigate becomes useRouter().push() from next/navigation, useParams still exists but the generic isn't supported — cast the result. <Link> swaps import but keeps semantics. Programmatic redirects in loaders become redirect() calls in server components or middleware.

Two traps worth flagging. First, catch-all client-side routing tricks — the 404.html redirect hack many deployed CRA apps use — are simply unnecessary now, delete them. Second, route guards implemented as wrapper components (<RequireAuth>) still work, but the App Router gives you a better seam: check the session in a layout or in middleware.ts and redirect before the page renders at all. Migrate the guards last, after the happy-path routes work, so auth bugs don't mask routing bugs.

The "use client" strategy that keeps you sane

The App Router's biggest conceptual shift is that components are server components by default — no state, no effects, no browser APIs. A CRA codebase is 100% client code, so a naive port produces a wall of errors. The pragmatic move is to invert the default at the boundaries: put "use client" at the top of each page's main interactive tree and get the app running first. This is not defeat — a fully-client Next.js app already beats CRA on routing, builds, and deployment — it's a working baseline you refactor from.

Then peel back selectively. Pages that fetch data on mount with useEffect + fetch are the best candidates: move the fetch into an async server component, pass data down as props, and the loading spinner disappears along with a client-side waterfall. Static content pages (marketing, docs, legal) drop "use client" entirely and become zero-JS by default. Leave genuinely interactive components — forms, dashboards, anything with framer-motion or browser APIs — as client components permanently; that's what they're for. The hydration gotchas live at this boundary: anything reading window, matchMedia, or localStorage must do so in useEffect, never during render, or the server and client render different trees.

Everything else, in one pass

Environment variables: REACT_APP_* becomes NEXT_PUBLIC_* for values the browser may see — and this is the moment to audit which ones actually should. CRA apps routinely ship secrets into the bundle because REACT_APP_ was the only option; in Next.js, a var without the prefix stays server-side, and API keys belong there, accessed via a route handler. Global CSS moves to an import in the root layout; CSS modules and Tailwind work unchanged. public/ assets carry over as-is, but <img> tags are worth upgrading to next/image on image-heavy pages.

Testing and tooling: Jest configs need the next/jest transformer, or take the opportunity to move to Vitest. react-helmet dies — static pages export a metadata object, dynamic pages export generateMetadata, and both are strictly better because the tags render on the server where crawlers see them. Deployment simplifies to next build on Vercel or any Node host. Budget roughly: routing and boot in the first day, page-by-page porting for the bulk of it, and a final pass for metadata and image optimization — a mid-sized CRA app is typically a one-to-two-week migration, not a rewrite.

Other migration guidesView all →
Related

Planning this migration?