DesignDev.io logoDesignDev.io logo

  1. Home
  2. /React Foundations
  3. /React Server Components Explained Without the Hype
← Back to home

React Server Components Explained Without the Hype

Alex Chen

Alex Chen

Cover Image for React Server Components Explained Without the Hype
Alex Chen

Alex Chen

January 2, 2026·5 min read
Updated April 26, 2026
Series:React Foundations
Level:Intermediate
Tags:reactweb
Assumes:React hooks familiarity, basic understanding of how React renders

React Server Components are one of the most significant shifts in React's model since hooks — and one of the most poorly explained. This is the mental model that actually makes them click, without the marketing language.


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/await directly — no useEffect, 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:

  1. Every component is a Server Component by default
  2. Add "use client" only when you need interactivity
  3. Fetch data directly in the Server Component that renders it
  4. 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.

Photo by Boitumelo on Unsplash


More Stuff

Building a Badge Component — Variants, Sizes, and Dot Indicators
April 3, 2026·3 min read

Building a Badge Component — Variants, Sizes, and Dot Indicators

Badge is the first purely presentational primitive in this series — no form integration, no event handling, no focus management. That simplicity is deceptive. A Badge that looks right in isolation often breaks inside a nav item, a table cell, or an avatar. The decisions that make it composable are worth getting right now.

Muhammad Athar

Muhammad Athar

Building a Label Component That Actually Connects to Its Field
April 3, 2026·3 min read

Building a Label Component That Actually Connects to Its Field

We've referenced a Label component in every form field article so far without building it. That's intentional — you need to understand what the Label connects to before you understand what it needs to do. Now that we have Input, Textarea, Checkbox, RadioGroup, Select, and Toggle, the Label's job is clear: it's not just styled text. It's the accessible name of a form field, and it carries required indicators, optional markers, and disabled state as part of that responsibility.

Muhammad Athar

Muhammad Athar

DesignDev.io logoDesignDev.io logo

Explore

  • Home
  • Search
  • Authors
  • Series
  • About Us

Account

  • Sign in

Legal

  • Privacy Policy
  • Terms of Service

© 2026 DesignDev.io · All rights reserved

Built with Next.js · Tailwind · Sanity · Vercel