React Server Components Explained Without the Hype

Every few months React ships something that splits the community. Server Components are different. They're not controversial because people disagree about whether they're useful — they're controversial because most articles about them are either marketing copy or so abstract they teach nothing.
Let me try a different approach.
Start with what React used to assume
Every React component runs in the browser. That was the model from 2013 until recently. Your component tree renders in a JavaScript runtime on the user's device. It fetches data from an API. It manages its own state.
This model has a fundamental cost: you have to ship all the JavaScript for all your components to the browser before anything renders. A component that formats a date using a 50KB library ships that library to every user, even if they never interact with that component.
React Server Components change one core assumption: not every component needs to run in the browser.
The actual mental model
A Server Component is a React component that runs once on the server and never ships its JavaScript to the client.
That's the whole thing. Everything else follows from that.
It can:
- Run
async/awaitdirectly — nouseEffect, no loading state - Access databases, file systems, and environment variables directly
- Import heavy libraries without adding a single byte to the client bundle
It cannot:
- Hold state (
useState) - Use browser APIs (
window,document) - Attach event handlers (
onClick,onChange)
Here's what that looks like in practice:
Notice AddToCartButton is a separate component. It needs an onClick handler, so it's a Client Component. The product data — which never changes based on user interaction — lives in the Server Component.
The boundary, not the wall
The most common misunderstanding about Server Components is treating the server/client split as a wall. It isn't. It's a boundary.
"use client" creates a Client Component. But it doesn't mean "everything inside this component is client-side only." It means "this is where the client boundary starts." Server Components can still be passed as children or props into Client Components.
Modal is a Client Component that needs useState. ServerRenderedContent is a Server Component. This works because children is passed as already-rendered output — it doesn't need to re-run on the client.
This is the pattern most tutorials get wrong. They say you can't use Server Components inside Client Components. You can — as long as they're passed in as props, not imported directly.
Where data fetching lives now
In the Pages Router, data fetching lived in getServerSideProps or getStaticProps — functions that ran on the server but were divorced from the component tree.
In the App Router with Server Components, data fetching lives directly in the component that needs the data:
Each component is responsible for its own data. React deduplicates identical requests automatically — if two components in the same render request the same data, it only hits the database once.
No prop drilling data down three levels. No overfetching at the page level to satisfy every child's needs.
The common mistakes
Putting "use client" on everything. I've seen codebases where every component starts with "use client" because the developer wasn't sure which needed it. This gives you the App Router with none of the benefits — everything ships to the client, nothing runs on the server.
The rule: only add "use client" when a component uses useState, useEffect, browser APIs, or event handlers. If it doesn't need any of those, leave it as a Server Component.
Putting "use client" on nothing. The other mistake. An entire app that's server-only has no interactivity. Every button, every form, every toggle needs to be a Client Component or composed from one. The App Router works because Server and Client Components compose together — not because you pick one.
Trying to pass non-serializable data from server to client. Server Components can pass data to Client Components as props — but only data that can be serialized over the network. Strings, numbers, objects, arrays — fine. Functions, class instances, Dates (in some cases) — not fine.
Fetching in Client Components when a Server Component would do. If a component doesn't need interactivity, don't make it a Client Component just to fetch with useEffect. Keep it a Server Component and fetch directly. This eliminates the loading state, the error boundary, and the extra network hop.
When Server Components genuinely help
They shine in three scenarios:
Content-heavy pages. Blog posts, product listings, documentation — anything where the data-to-interactivity ratio is high. The page ships minimal JS because all the rendering happened on the server.
Removing dependencies from the bundle. A markdown renderer, a syntax highlighter, a date formatting library — if it only runs at render time, put it in a Server Component and pay zero client-bundle cost.
Eliminating API routes for internal data. Instead of a Next.js API route that fetches from your own database and returns JSON to your own frontend, fetch directly in a Server Component. One less network hop, one less file to maintain.
When they don't help
Highly interactive UIs. A rich text editor, a drag-and-drop interface, a real-time collaborative feature — these are Client Components. Server Components don't make interactive UIs better.
Data that changes based on user input. A filtered list where the filter lives in useState needs to be a Client Component. You can't re-run a Server Component from the browser in response to a state change.
Small projects with simple data needs. The App Router and Server Components add mental overhead. A personal blog or a simple form-based app might not need them. The Pages Router still works and is still supported.
The honest performance picture
Server Components aren't always faster. They're faster for specific patterns:
- First load on content-heavy pages: faster (less JS to download and parse)
- Subsequent navigation: about the same (router cache handles most of it)
- Highly interactive UIs: no change (they're Client Components anyway)
The real win isn't raw performance metrics. It's that the default is now correct. You don't have to manually optimize for bundle size — Server Components don't add to it. You don't have to manually set up loading states for server data — Suspense handles it. The pit of success got deeper.
Starting point
If you're coming from the Pages Router, the transition that clicks everything into place is this:
- Every component is a Server Component by default
- Add
"use client"only when you need interactivity - Fetch data directly in the Server Component that renders it
- Pass that data to Client Components as props when interactivity is needed
The rest — caching, streaming, Suspense boundaries — builds on top of that foundation. Get the mental model right first, then layer in the details.
Up next: The key prop is not just for lists — the use case that solves a whole category of React bugs in one line.
Related: How to handle authentication in Next.js App Router — auth in the App Router without NextAuth doing everything.
Alex Chen is a senior frontend engineer who writes about React patterns, JavaScript internals, and the decisions that separate maintainable codebases from ones that fight back. Opinionated by design.

