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
- Inventory CRA magic.
%PUBLIC_URL%inindex.html,REACT_APP_*environment variables,src/setupTests.js, theproxyfield inpackage.json, absolute imports viajsconfig/tsconfig. Each has a Vite equivalent; listing them first is what makes the migration boring, which is the goal. - Move
index.htmlto the project root and add<script type="module" src="/src/index.tsx">. Vite treats the HTML file as the entry point. - Swap the toolchain. Remove
react-scripts; addviteand@vitejs/plugin-react. Scripts becomevite,vite build,vite preview. - Translate the environment.
REACT_APP_FOObecomesVITE_FOO, read viaimport.meta.env.VITE_FOO. Grep forprocess.env— in Vite it does not exist in the browser, and stray references are the most common post-migration runtime error. - Port the dev proxy to
server.proxyinvite.config.ts— same shape, different file. - 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.
- Verify the build honestly.
vite build, then servedist/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.