Create React App Deprecation and Modern Starts
If you're starting a new React app in 2026, don't use Create React App.
CRA is deprecated now. And yeah, this can be a little confusing because there's still plenty of tutorials out there starting with npx create-react-app, old Stack Overflow answers use it everywhere, and existing CRA projects are still running completely fine.
So deprecated doesn't mean "this command suddenly stopped working". It means React no longer recommends CRA for starting new applications, and you should pick something current for new work.
Which brings up the obvious question: what do we use now?
Well, that depends on what you're actually building.
React itself gives you the UI part. Components, state, JSX, rendering and so on. But a real application also needs some setup around that. Something has to transform your source code during development, produce files for production, maybe handle routing, maybe load data, maybe render on the server, maybe help with deployment.
CRA used to give React developers a ready-made setup for a client-side app without making them configure webpack and Babel themselves.
That was very convenient for a long time.
Now CRA is in maintenance mode, and for new apps we have better options depending on what the app needs.
How do I know if a project is using Create React App?
Don't guess based on the folder names.
A CRA project may have src, public, App.js, index.js and all the usual files, but plenty of non-CRA projects can look almost the same.
Open package.json and look for react-scripts.
{
"dependencies": {
"react-scripts": "5.0.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build"
}
}If you see that, you're almost certainly looking at a Create React App project.
react-scripts was doing a lot of work for CRA. When you ran:
npm startor:
npm run buildyou weren't directly configuring webpack, Babel, ESLint and all those tools yourself. CRA kept most of that configuration inside react-scripts.
Which was nice when the defaults worked for you.
When they didn't... well, then things got less nice.
CRA did allow something called eject:
npm run ejectThat copied a bunch of the previously hidden configuration into your project so you could edit it yourself.
If you find an old project that has been ejected, don't really treat it as a normal CRA project anymore. At that point it may have custom webpack config, Babel config, Jest setup, ESLint changes, loaders, plugins, maybe five years of somebody saying "just one small change here".
So yeah, inspect what is actually there.
Also, having a public folder doesn't prove anything. Vite projects can have one too. Check package.json.
What does CRA being deprecated actually mean?
The React team deprecated Create React App for new applications in February 2025.
That doesn't mean all deployed CRA websites stopped working the next morning.
Your browser doesn't know or care that React deprecated the tool which created those JavaScript files. If the production build is already sitting on a server and those files still work in the browser, they keep working.
Same with an existing local project. If its dependencies still install correctly with your Node version and the rest of your setup, you can still run it.
So if your company has some old dashboard built with CRA, don't see the deprecation notice and immediately spend Friday afternoon replacing half the build system.
First understand the current project.
Does npm ci still work? Does the test suite pass? What environment variables does it use? Does it depend on CRA's dev proxy? Does it use %PUBLIC_URL% somewhere? Does it have a service worker? What does the production build look like?
Get those answers before changing tooling.
Because migrating the toolchain without knowing what the old toolchain was doing is how you get one of those "works on localhost" migrations which fails immediately after deployment.
Existing CRA apps can keep running, but keeping one forever isn't a great plan either. The tooling gets older while the rest of the JavaScript world keeps changing, and eventually you're trying to combine newer packages with assumptions from an older build setup.
So for old projects, migration is something you plan. For new projects, just don't start with CRA.
Why does React recommend frameworks now?
If you go through the current React docs for starting a new app, you'll see frameworks recommended for many applications.
Why?
Because most serious applications eventually need more than rendering components into one browser page.
You might need routing. Then nested routes. Then data fetching for those routes. Maybe some pages should render on the server. Maybe some should be generated ahead of time. You may need route-level code loading, metadata, form mutations, caching or deployment support.
You can absolutely assemble those pieces yourself.
React isn't stopping you.
But then you're choosing a router, choosing your data-loading setup, deciding your server-rendering setup if you need one, deciding how builds get deployed, and making sure all those pieces continue working together.
A framework already makes many of those application-level decisions.
The React docs currently point to options including Next.js App Router and React Router in framework mode.
And using a framework doesn't automatically mean every React component is suddenly server-rendered. Different frameworks support different rendering modes, and even inside one app you may have client-rendered and server-rendered parts.
The bigger change is that you're accepting more of the application's setup from the framework instead of assembling each part yourself.
Then why are we using Vite in this book?
Because I want you to actually see what starts the React app.
With Vite, the startup path stays pretty obvious:
index.html
-> main.tsx
-> createRoot
-> AppYou can open index.html. You can open main.tsx. You can literally see the DOM element being selected and passed to createRoot().
Nothing mysterious there.
And for learning React, that's useful because we don't also have to explain generated route files, server entry files, loaders and framework conventions before you've even rendered your first component.
Vite gives us a development server and build tooling, but it doesn't try to become the whole application architecture.
That also makes it completely valid for many real apps.
If you're building a client-side internal dashboard which talks to an API, maybe all you need is Vite, React, a router and whatever libraries your project actually uses.
No problem.
But Vite won't make your routing decisions for you. It won't design your data-loading system. It won't automatically give you server rendering. It won't decide your authentication model or deployment architecture.
You pick those pieces yourself.
When I say we're doing a "scratch setup", I don't mean we're sitting here writing our own JavaScript bundler at 2 AM.
We're starting with a build tool and then adding the libraries the application needs instead of starting inside a full application framework.
React and your starter are two different things
Take this component:
function LessonCard({ title }: { title: string }) {
return <h2>{title}</h2>;
}Nothing here is Vite-specific.
You could put this component inside a Vite app, Next.js app, React Router framework app, or some other React setup and the component itself doesn't suddenly become different React code.
React is handling things such as components, props, state, JSX and rendering.
Your tooling has other jobs. It decides how .tsx files get transformed, how environment variables are exposed, what command starts the development server, where production files get written, and so on.
Frameworks add more application behavior around that. For example, a framework route may decide when LessonCard appears.
And below all of this, the browser is still creating DOM nodes, applying CSS and handling user input.
You can roughly separate it like this:
React concern: component returns UI from props
Vite concern: .tsx is transformed and served
framework concern: route decides when component appears
host concern: browser creates DOM and handles inputThis separation becomes really useful when you're debugging migrations.
Suppose the app worked before moving from CRA to Vite, and now images are loading from the wrong URL. That's probably not a React state problem.
Or maybe the build works perfectly but one component receives the wrong prop. Changing the Vite config isn't going to fix that.
Try to identify which part of the system actually owns the behavior you're debugging.
Pick the setup based on what you're building
Don't start with "should I use Vite or Next.js?"
Start with "what does this app need?"
Say you're building a small internal admin tool. It's fully client-rendered, sits behind company login, talks to an existing API, and gets deployed as static files. Vite plus a browser router may be completely enough.
Now suppose you're building a public commerce site. You need server-rendered pages, route-based data loading, metadata, caching and server-side mutations. A framework starts making a lot more sense because those jobs already need to work together.
Or maybe you're building a React widget which gets inserted into one part of an existing non-React website. You probably don't need a full-site framework at all. You need a client entry which mounts React into one DOM element, and perhaps a library build.
And if you're publishing a reusable React component package to npm, that's another setup again. You're not really building an application there. You need package exports, generated type declarations, a library build and probably some separate app where you can test or demonstrate the components.
So there isn't one "correct React starter".
Before choosing one, write down what you actually need:
rendering: client only, server, static, or mixed
routing: none, browser routes, or server routes
data: local, API, route loaders, or server reads
deployment: static files, Node server, edge runtime, or platform adapter
maintenance: which team maintains each added libraryOnce you have those answers, choosing the setup gets much easier.
Before migrating a CRA project, make sure the old one actually builds
This is one step people skip and then regret.
Before touching the toolchain, get the current project into a known working state.
Usually you'll start with something along these lines:
npm ci
npm test -- --watchAll=false
npm run buildMaybe your project's test script is different. That's fine. The important part is knowing what currently passes before you change anything.
If the old production build already fails, and then you migrate to Vite and the new build also fails, now you don't even know whether your migration introduced the bug.
So record the working commands and then start checking CRA-specific behavior.
Environment variables
CRA exposes custom browser variables using the REACT_APP_ prefix.
For example:
REACT_APP_API_ORIGIN=https://api.example.comAnd your source might read it through process.env.
Vite does this differently. By default, client variables use the VITE_ prefix:
VITE_API_ORIGIN=https://api.example.comand your code reads them through:
import.meta.env.VITE_API_ORIGINDon't only rename the variable in your local .env file and call the migration done.
Production may be getting that variable from GitHub Actions, Docker, a hosting dashboard, Kubernetes configuration, or whatever your team uses.
You have to update those places too.
HTML placeholders
CRA projects often have public/index.html, and you may find placeholders such as:
%PUBLIC_URL%Vite handles index.html differently and normally keeps it at the project root.
So before moving the HTML file around, search it for CRA-specific placeholders and understand what they're doing.
Development proxy
Some CRA apps have this in package.json:
{
"proxy": "http://localhost:4000"
}Others use custom proxy middleware.
If the frontend is calling /api/users during development and CRA was forwarding that request to some backend server, your new Vite setup needs equivalent behavior if the application still depends on it.
Also check cookies, rewritten paths and headers. Proxy setups can have more going on than one URL.
Asset imports
Assets are another area where "the files look the same" doesn't guarantee the toolchains behave the same.
Check image imports, public URLs, fonts, CSS imports and especially SVG handling.
Some CRA setups let you import SVG files as React components using tool-specific behavior. Moving the source files into Vite doesn't automatically reproduce every CRA transform.
Tests and service workers
CRA projects commonly use Jest through react-scripts test, and some older projects also have service worker setup from CRA templates.
Vite doesn't automatically migrate either of those.
If the existing application has tests, preserve the tests while changing how they're run. And if it uses a service worker, find out what that worker currently does before removing or replacing it.
Change the tooling first, not the whole application
When migrating CRA to Vite, try to make the first pass boring.
Boring is good here.
Create the Vite index.html, install Vite and the React plugin, connect the existing React entry file, update environment variable access, replace CRA HTML placeholders, reproduce any proxy behavior you still need, fix asset handling, and get the existing tests running through a maintained setup.
Then make a production build and compare it with what the CRA build used to do.
Don't also decide this is the perfect time to replace React Router, rewrite state management, convert every CSS file, reorganize every folder and redesign the component tree.
Now if something breaks you have ten possible causes.
Do the tooling migration first. Get it working. Then do application rewrites separately.
Also don't stop testing just because npm run dev opens a page that looks correct.
Run the production build. Test direct URL loads if you have client routing. Check production environment variables. Check asset paths. Run the tests. Check deployment headers if your host depends on them.
Development and production aren't the same setup.
Don't eject just because you're planning to migrate
If the CRA project has never been ejected, don't run:
npm run ejectas some preparation step for migration.
Ejecting copies CRA's old build configuration into your repository, which now gives you even more configuration to inspect and maintain.
You don't need that just to move to Vite or a framework.
Migrate the unejected CRA project directly.
If somebody already ejected it years ago, then you have a different job. Inspect the actual webpack, Babel, Jest and ESLint configuration because the project may no longer behave the same way a normal CRA project does.
There may be local modifications in there which the migration needs to reproduce.
So what counts as a modern React start?
For me, "modern" doesn't mean somebody uploaded a tutorial three weeks ago.
Use software versions which are still maintained. Follow the current documentation for the tools you're actually using. Know what your build command does. Have type checking and linting if the project needs them. Make sure the production build really runs.
For this project we're using React 19.2.x, Vite 8, a supported Node.js release, TypeScript checking, Oxlint, and an actual production build.
Could you choose something else?
Of course.
The point isn't that every React app now has to use the exact same stack.
The point is that CRA isn't the default starting point anymore, and whatever replaces it should be chosen because it fits the application you're building, not because one command happened to be popular five years ago.