The Next.js + React stack explained: what each layer does and when it's wrong
By Nayan Kyada · · 8 min read
Part of Next.js Performance
The Next React stack is what most product teams land on after trying a few alternatives — React for the UI layer, Next.js as the framework, a headless CMS like Sanity for content, Tailwind for styling, and Vercel or a similar platform for deployment. Each of those choices is independent, but they've converged into a default for a reason. This guide explains what each layer actually does, why the combination became dominant, and — just as importantly — the situations where this stack is the wrong call.
What "the Next React stack" actually means
People use "Next.js stack" and "React stack" interchangeably, but there's a useful distinction. React is a UI library — it renders components, manages local state, and handles events in the browser. It has no opinion about routing, data fetching, or how you ship to production. Next.js is the opinionated framework built on top of React that fills those gaps: file-system routing, server components, ISR, image optimisation, metadata API, and so on.
So when a team says "we're on Next React", they usually mean:
- React 19 — the UI primitives (hooks, server components, Suspense boundaries)
- Next.js App Router — routing, rendering strategy, API/route handlers
- A headless CMS — Sanity, Contentful, or Payload for structured content
- Tailwind CSS — utility-first styling (v4 as of 2026)
- Vercel or Cloudflare — edge deployment, CDN, and preview environments
Those five layers cover the vast majority of what a product or marketing site needs. The rest — search, email, video, auth — gets bolted on as needed.
React: the UI layer
React's job is to describe what the screen looks like as a function of data. With React 19 and the App Router, a large portion of that work now happens on the server. React Server Components (RSC) fetch data and render HTML without sending any JavaScript to the browser — which is a meaningful shift from the client-heavy SPAs React was associated with before 2023.
The practical result: a product page that queries your CMS and renders a list of articles ships zero JavaScript for the article list itself. Only the parts that need interactivity — a search input, a like button — stay as client components.
This isn't free complexity reduction. RSC introduces a clear mental model split: server components cannot use browser APIs, hooks, or event handlers. Client components must be explicitly marked with 'use client'. Teams unfamiliar with this boundary tend to scatter 'use client' everywhere, negating the performance benefit. Getting comfortable with that boundary is the main learning curve in 2026.
Next.js: the framework layer
Next.js sits above React and makes decisions you'd otherwise make yourself:
Routing — The App Router maps the app/ directory to URL segments. Dynamic routes (app/posts/[slug]/page.tsx), catch-all routes, route groups, parallel routes, and intercepted routes cover nearly every URL shape without a separate router library.
Rendering strategy — Each page or segment can be statically generated, server-rendered per request, or incrementally revalidated. With Partial Prerendering (PPR), you can prerender the shell and stream dynamic parts — so a blog post's static body serves from the edge CDN while the dynamic comment count streams in separately.
Image and font — next/image handles format negotiation (WebP/AVIF), lazy loading, and CLS prevention via reserved space. next/font subsetting eliminates render-blocking font requests. Both are opt-in but the defaults are good.
Metadata API — The generateMetadata function in each route segment lets you return <title>, Open Graph tags, and canonical URLs with access to route params and async data — so page titles and descriptions come from your CMS without a third-party library.
// app/posts/[slug]/page.tsx
import { client } from '@/sanity/lib/client'
interface Props {
params: { slug: string }
}
export async function generateMetadata({ params }: Props) {
const post = await client.fetch<{ title: string; description: string }>(
`*[_type == "post" && slug.current == $slug][0]{ title, description }`,
{ slug: params.slug }
)
return {
title: post.title,
description: post.description,
}
}
export default async function PostPage({ params }: Props) {
const post = await client.fetch(
`*[_type == "post" && slug.current == $slug][0]{ title, body }`,
{ slug: params.slug }
)
return <article>{/* render post */}</article>
}The CMS layer
Next.js doesn't care where your data comes from, but most teams need structured content — blog posts, product pages, landing page blocks — managed by non-developers. That's where a headless CMS fits in.
Sanity is the most common pairing in this stack because it exposes a real-time content API with typed GROQ queries, stores images on a CDN with on-the-fly transforms, and runs Sanity Studio (the editor UI) either at a separate URL or embedded at /studio inside your Next.js app. The schema lives in code, not in a GUI, which means it's version-controlled alongside your components.
Contentful and Payload are viable alternatives depending on budget and self-hosting preference — but the integration pattern with Next.js is roughly the same: query the CMS in a server component, pass typed props to client components, revalidate on webhook.
Styling: Tailwind CSS v4
Tailwind v4 dropped the tailwind.config.js file in favour of CSS-native configuration via @theme in a root CSS file. In a Next.js project, that means adding one import to app/globals.css and writing utility classes the same way you always did — no breaking change to the component API, just a faster build.
The reason Tailwind became the default in this stack: it eliminates the runtime cost of CSS-in-JS (no hydration penalty, no style injection on the client), produces near-zero unused CSS in production thanks to the scanner, and keeps styling colocated with markup without a separate .module.css file per component.
Hosting: Vercel vs the alternatives
Vercel is the canonical deployment platform for Next.js — it was built by the same team, and features like ISR, PPR, edge middleware, and image optimisation are tested there first. For most teams, the zero-configuration deploy from a GitHub push, plus per-branch preview URLs, is worth the cost until scale makes the per-seat pricing awkward (the hobby-to-pro gap is around $20/month; team pricing scales from there).
Cloudflare Pages supports Next.js with the @cloudflare/next-on-pages adapter. It's cheaper at scale and has better global edge coverage, but some App Router features — specifically certain streaming and middleware behaviours — have lagged behind Vercel's support. AWS Amplify Gen 2 and Railway are options for teams that need VPC-adjacent deployment or want to avoid Vercel lock-in entirely.
Why this combination won
The short answer: each layer solved a real problem that its predecessor left open.
Create React App gave you React without a build opinion, but nothing else — no SSR, no routing, no image handling. Gatsby added static generation but required a GraphQL layer for everything, which felt overengineered for most content sites. Next.js Pages Router fixed Gatsby's data fetching complexity while keeping SSR an option. The App Router completed the picture by making server components the default, which finally made the "zero JS for static content" promise practical rather than theoretical.
Tailwind replaced CSS-in-JS libraries that were introducing hydration overhead and unpredictable specificity. Sanity replaced WordPress for teams that needed structured content without PHP. Vercel replaced DigitalOcean droplets for teams that didn't want to manage Nginx configs.
Each swap was motivated by a concrete pain point, not trend-chasing. That's why the stack has staying power.
When this stack is the wrong choice
The Next React stack is not universally correct. Three scenarios where you should think twice:
Mostly static, no interactivity. If you're shipping a documentation site or a blog with no dynamic personalisation, no real-time content, and no complex UI, Astro will produce smaller bundles and simpler deployment with less configuration. Next.js adds framework overhead that documentation sites don't need.
Highly interactive, app-like UI. If your product is closer to Figma than to a marketing site — lots of canvas interaction, real-time collaboration, drag-and-drop at scale — React's concurrent model is still the right choice but you may not need Next.js's server rendering machinery at all. A Vite-built SPA with a dedicated API backend is simpler to reason about.
Very small teams with no React experience. The App Router's RSC model has a non-trivial learning curve. If your team is two developers who know Vue well and need to ship in six weeks, adopting Next.js will slow you down. The stack rewards teams who already know React; it taxes teams who don't.
Putting the layers together
A minimal Next React stack project structure looks like this:
app/
layout.tsx # root layout, fonts, global CSS
page.tsx # home page — server component
posts/
[slug]/
page.tsx # post detail — server component + generateMetadata
sanity/
lib/client.ts # Sanity client config
schemas/ # document type schemas
components/
ui/ # Radix-based primitives
blocks/ # CMS-driven page sections
public/
tailwind.css # @import "tailwindcss"; @theme { ... }The app/ tree handles routing and rendering. The sanity/ tree owns the CMS integration — client, schemas, and GROQ query helpers. Components are split between generic UI primitives (usually Radix-wrapped) and CMS-driven blocks that map a content type to a layout.
This separation isn't arbitrary. When a designer changes a component, they touch components/ui. When an editor adds a new content block type, the schema and the block component change together, nothing else. That's the modularity the stack is designed to produce — and it holds as long as each layer stays in its lane.