When plain React is the better choice over Next.js
Next.js is the default answer for React projects, yet many mature products still run on plain React. What I learned from both sides, and the one question I now ask before picking.
Next.js has become the default answer for React projects. Tutorials start with it. The React docs suggest starting with a framework. Many teams pick it without a second thought.
But a lot of mature products, the kind people log into every single day, still run on plain React. That's not because those teams are behind. For their kind of app, plain React is simply the better fit.
This post is about when that's true, told through what I've seen on real projects.
When Next.js is the default for everything#
For a long time, every new project I saw started the same way.
npx create-next-app.
Nobody questioned it. Next.js was the modern way, plain React was the old way. I picked that up without ever being told.
In my head, Next.js was not a choice. It was just how you write React.
When the framework becomes the work#
Later I worked on a product where Next.js was everywhere. The marketing site was Next.js, and the product console that customers logged into was Next.js too.
For the marketing site, it made perfect sense. That site needs to show up on Google. Next.js renders the page on the server, so Google gets real content instead of an empty page. That's exactly what it's good at.
The console was a different story.
I didn't see every corner of that setup. But one thing was hard to miss: a lot of our time went into Next.js itself, not into the product.
A simple feature would turn into a framework question. Should this page be built ahead of time, or on every request? Should this be a server component or a client one? Where does "use server" go?
Then came the move from the old pages setup to the new app router. Old links still had to work, so redirects got added. First inside the Next.js app. Then again at the server sitting in front of it. Two layers of redirects for one product.
And then the part that surprised me most.
Server actions and API routes, two of the biggest reasons to pick Next.js, were switched off at deployment. They clashed with how requests were routed internally.
So we were paying for a framework whose main server features we couldn't even use.
Things broke often, and when they did, it was hard to tell which layer caused it.
And all of it sat behind a login that Google never sees.
I don't blame the people there. Every decision made sense on its own day. But we were carrying all the weight of Next.js and getting very little of what it's built for.
When plain React looks behind, but isn't#
Then I moved to a codebase that was the complete opposite.
No Next.js. No server components. A plain React app. And React 18, not even the latest version.
Honestly, my first reaction was: where is the engineering here? This felt behind.
A few months in, I changed my mind. It turned out to be one of the most mature codebases I have worked in.
The maturity wasn't in the framework. It was in the decisions around it:
One repo for many apps. All the frontend apps lived together in a single repository, so shared code had one home.
One version of each library. Common libraries were installed once at the top of the repo, and every app used that same version. No app quietly drifting onto its own copy, no "works in this app, breaks in that one."
A real design system. Buttons, inputs, tables and the rest came from an internal component package. Nobody rebuilt a dropdown for the fifth time.
Tokens for colors. No random hex codes scattered around. Colors came from named tokens, so the whole product looked like one product.
Careful choices about what becomes shared. This is the part I like most. Not everything got turned into a package. Someone actually decided what deserved to be shared across apps and what should stay inside one app.
The backend had the same habit. It was split into many small services, and each new one started with a real decision: Go, Node or Python, based on what that service needed. Not on what was trending.
Every choice was made on purpose, and that's what made the codebase feel strong.
The team spent its time on the product, not on the framework. When something breaks, there are fewer places to look. And the "old" React 18 turned out to be just one version behind. It didn't slow anyone down, because the version was never the thing that mattered.
What I got wrong about React and Next.js#
Looking back, my mistake started early. I thought of React and Next.js as two options where one is newer and better.
That's not how it works. Next.js is built on top of React. In a Next.js app you still write React components, same JSX, same hooks, same state.
React is the engine. Next.js is a car built around that engine, with extra parts already fitted: routing from folders, server rendering, static pages, backend routes.
So the real question was never "React or Next.js?"
It was "do I need the extra parts?"
Nobody searches for their dashboard#
The biggest extra part is server rendering. And server rendering mostly helps with one thing: strangers finding your page on Google and seeing it fast on their first visit.
Now think about the tools you use at work.
Nobody searches Google for:
"Jira issue dashboard"
or
"Slack workspace"
You open a bookmark or a tab that's already open. You log in once and stay for hours.
Apps behind a login can't be seen by Google, and they don't need to be. After the first load, a plain React app is already fast, because every screen change happens right in the browser.
A plain React app is also just a set of static files once it's built. Put them on a file host and you're done. No server to run, no server to scale, no second layer of redirects.
That's why the marketing site I mentioned earlier made sense on Next.js, and the console didn't really need it. Same company, two different jobs, and they needed two different tools.
When I would still pick Next.js#
I don't want this to sound like "Next.js is bad." It isn't. I'd still pick it when:
The page is public and needs to be found: a marketing site, a blog, docs.
Some app pages are shared as public links that people open without logging in.
It's a solo side project and I want frontend and small APIs in one place.
The team already knows it deeply, and has a setup that doesn't fight it.
I actually picked Next.js myself for FreeClothes.in. There, every clothing item has its own public page. Each one needs its own title, description and preview image, so it shows up properly on Google and looks right when someone shares the link.
That's exactly the job server rendering was made for. Every page is meant to be found by a stranger. Plain React would have fought me on that. Next.js made it the default.
The question I ask now#
What changed for me was not my framework knowledge. It was the question I ask first.
Who opens this page, and how do they get there?
If strangers arrive from Google, server rendering earns its place. If people log in and stay all day, plain React is usually enough, and much simpler to live with.
The codebase I once thought was "behind" had just asked that question earlier than I did.
Keep reading