Leaving Create React App: Vite or Next.js?

Create React App was the right default for years, and a lot of good software still runs on it. But it is no longer maintained, the React team's own documentation steers new projects elsewhere, and every month a CRA app stays put, its dependency tree drifts further from the ecosystem's center of gravity. The question for teams holding a CRA codebase is not whether to move but where — and the answer is not the same for everyone.

The actual decision: Vite or Next.js

Strip away the discourse and there are two sensible exits.

Vite is the like-for-like exit. Your app stays a single-page application: same routing library, same data fetching, same deployment (static files behind a CDN), same mental model. What changes is the toolchain — dev server startup in milliseconds, dramatically faster HMR, a maintained plugin ecosystem, and modern build output. Migration risk is low and the payoff is immediate developer experience.

Next.js is an architecture change wearing a build tool's clothes. You gain file-based routing, server-side rendering, React Server Components, API routes, and image/font optimization — and you take on a server runtime (or a hosting platform that abstracts one), a framework's conventions, and a bigger migration.

The honest selector questions:

  • Does the app need server rendering? If SEO, social previews, or first-paint performance on content pages matter — marketing pages, e-commerce catalogs, media — that is Next.js territory. A dashboard behind a login gains almost nothing from SSR.
  • Is the app's routing and data layer healthy? If React Router and your query layer serve you well, Vite preserves them untouched. Next.js replaces routing outright and reshapes data fetching.
  • How much migration budget exists? A Vite exit is typically measured in days; a Next.js exit is a project. Choosing Next.js "while we're at it" without budgeting for a project is how migrations stall at 60%.

A useful rule from our migration work: do not bundle the framework decision into the tooling decision. If you are unsure, exit to Vite first — it is quick, low-risk, and leaves every future option open, including a later move to Next.js route by route.

The Vite exit, step by step

  1. Inventory CRA magic. %PUBLIC_URL% in index.html, REACT_APP_* environment variables, src/setupTests.js, the proxy field in package.json, absolute imports via jsconfig/tsconfig. Each has a Vite equivalent; listing them first is what makes the migration boring, which is the goal.
  2. Move index.html to the project root and add <script type="module" src="/src/index.tsx">. Vite treats the HTML file as the entry point.
  3. Swap the toolchain. Remove react-scripts; add vite and @vitejs/plugin-react. Scripts become vite, vite build, vite preview.
  4. Translate the environment. REACT_APP_FOO becomes VITE_FOO, read via import.meta.env.VITE_FOO. Grep for process.env — in Vite it does not exist in the browser, and stray references are the most common post-migration runtime error.
  5. Port the dev proxy to server.proxy in vite.config.ts — same shape, different file.
  6. Replace Jest with Vitest. Vitest reads your Vite config, so transforms and aliases come free; the assertion API is Jest-compatible, and for most suites the change is configuration plus a handful of mock-syntax edits.
  7. Verify the build honestly. vite build, then serve dist/ and click through the app. Compare the output bundle sizes with your CRA build — teams usually see equal or smaller output, and should investigate if not.

Steps 1–7 are commonly a single pull request for a mid-sized app.

The Next.js exit, without the big bang

If the selector questions point to Next.js, resist the rewrite instinct. The App Router lets a CRA app move incrementally: mount the existing SPA under a catch-all route so everything works on day one, then promote routes out of the SPA one at a time — each becoming a real server-rendered route with its own data fetching, each shippable on its own. Routing and navigation move first; server components and data-layer changes come per-route, after the route works. The strangler pattern is slower on the calendar than a rewrite promises to be, and faster than a rewrite actually is.

What "done" looks like

Either way, the exit is complete when react-scripts is gone from the lockfile, CI runs the new build and tests, the deployment pipeline serves the new output, and — easy to forget — the README no longer lies about how to run the project. Budget an afternoon for that last item. The next engineer to join will spend their first hour with it.