Next.js vs plain React for production apps: what 6 years of client work taught me
"Should we use Next.js or just React?" is one of the most common questions I get from clients. After six years shipping both to production, my honest answer is that they're not really competitors — but the choice still matters, and getting it wrong is expensive. Here's how I actually decide.
They're not the same category
React is a library for building user interfaces. Next.js is a framework built on React that adds routing, server rendering, data fetching, bundling, and a deployment model. So "Next.js vs React" is really "Next.js vs React plus a pile of decisions you now have to make yourself." The question isn't which is better — it's whether you want those decisions made for you.
What plain React (with Vite) still wins at
- Internal tools and dashboards behind a login, where SEO is irrelevant and you want the leanest possible setup.
- Embedded widgets that mount into someone else's page — you don't want a framework's routing and rendering assumptions.
- Highly interactive single-page apps — editors, design tools, dashboards — that are basically one big client-side application.
- Learning and prototyping, where Vite's instant dev server and minimal config get you moving in seconds.
For these, I'll reach for React with Vite and a router, and I won't feel like I'm missing anything. Adding Next.js here is complexity you pay for and never use.
What Next.js gives you that you'd otherwise rebuild
The moment a project needs to be found in search or load fast on a first visit, plain React starts costing you. A client-only React app ships an empty HTML shell and fills it in with JavaScript — bad for SEO and for the first paint. Next.js renders on the server so the page arrives populated. Compare the data-fetching story:
// Plain React: fetch after the component mounts, on the client.
function Products() {
const [products, setProducts] = useState([]);
useEffect(() => {
fetch('/api/products').then(r => r.json()).then(setProducts);
}, []);
// First render is empty. Crawlers and slow phones see nothing yet.
return <List items={products} />;
}
// Next.js: fetch on the server, ship populated HTML.
export default async function Products() {
const products = await db.product.findMany();
// The HTML already contains the products. Fast, indexable.
return <List items={products} />;
}You *can* recreate server rendering, code splitting, and image optimization in a plain React setup. I've done it. But you're now maintaining infrastructure that Next.js gives you for free and keeps updated.
The SEO gap is usually the decider
For marketing sites, e-commerce, blogs, marketplaces, and anything that lives on organic traffic, I default to Next.js — not because it's fashionable, but because server-rendered, indexable pages with clean metadata and structured data are the difference between ranking and being invisible. This very portfolio is a statically exported Next.js site for exactly that reason.
The cost nobody mentions: complexity
Next.js is not free to adopt. The App Router, Server versus Client Components, caching behaviour, and the server/client boundary are real concepts your team has to learn. I've watched teams ship subtle bugs by treating a Server Component like a Client one. If your app genuinely doesn't need server rendering, that learning curve buys you nothing.
How I choose, in one paragraph
If the project needs SEO, fast first loads, or server-side data fetching, I use Next.js. If it's a login-walled tool, a widget, or a heavily interactive SPA where none of that matters, I use React with Vite and keep the stack lean. I don't pick the framework I like most; I pick the one whose strengths match what the project actually needs — and I'll tell a client honestly when the trendy choice is the wrong one.