React Server Components: What No One Tells You

Everyone has an opinion on React Server Components.

Some say it's the future. Some say it's overengineered. Most tutorials explain what RSC is, walk you through a demo, and call it a day. What they don't cover is what actually changes when you adopt it in a real project — the mental model shifts, the footguns, and the moments where you'll question every architectural choice you've ever made.

I've been using RSC in production through Next.js 15 and 16, and I want to give you the honest version.


The Mental Model Shift is Real

Before RSC, the React mental model was simple: components run in the browser, they can use state and effects, and that's that. Server rendering (SSR) was a hydration detail — the same component rendered twice, once on the server, once on the client.

RSC breaks this in a meaningful way. Server Components never run in the browser. They don't hydrate. They can't use useState, useEffect, or any browser API. In return, they can do things client components can't — read from databases directly, access environment secrets, reduce the JavaScript shipped to the browser.

The moment this clicked for me was when I stopped thinking of it as "which components should be server vs client" and started thinking of it as two different component types with different capabilities — like two different tools. A server component is more like a server endpoint that returns JSX. A client component is the React you already know.

Once that mental model lands, a lot of things start to make sense.


The Footguns Nobody Warned Me About

1. The Boundary is Contagious

Once you import a client component, everything it imports must also be compatible with the client. This is obvious in hindsight, but in practice it means a single third-party library that uses useEffect internally can force an entire subtree into client-land.

I've had cases where I wanted a lightweight UI component from a library only to discover it dragged in browser-only globals — and suddenly a page I assumed was mostly server-rendered had a much larger client bundle than expected.

The fix: Use 'use client' boundaries deliberately. Import heavy client components at leaf nodes, not at the top of your component tree. And check if a library has a server-compatible version before reaching for it.

2. Async Waterfalls Are Still a Problem

RSC lets you await data directly inside components, which feels clean. But if multiple server components each fetch their own data in sequence, you get the same waterfall problem you'd have with useEffect chains.

// This fetches sequentially — each awaits the one above it
async function Page() {
  const user = await getUser()
  const posts = await getPosts(user.id)
  return <PostList posts={posts} />
}

The solution is to fetch in parallel where you can:

async function Page() {
  const [user, settings] = await Promise.all([getUser(), getSettings()])
  return <Profile user={user} settings={settings} />
}

It sounds obvious, but when you're used to co-locating data fetching with components, it's easy to accidentally chain fetches across a component tree.

3. Debugging is Still Rough

Server component errors don't surface the same way client errors do. In development you get useful stack traces, but they often point into framework internals rather than your code. In production, if you're not set up with proper logging, a failure in a server component can be silent or cryptic.

I learned to be explicit about error boundaries and to add try/catch around data fetches in server components — not because it's always necessary, but because it gives you something actionable when things go wrong.


When RSC Actually Helps

It's not all footguns. RSC has genuinely improved things in ways I didn't anticipate.

Smaller bundles. Components that are purely presentational and data-driven — think blog post layouts, article pages, stats dashboards — don't need to ship any JavaScript. This portfolio site uses RSC for article rendering and most of the static pages. The result is leaner pages with less JavaScript for the browser to parse.

Simpler data fetching. No more useEffect + loading state + error state for data that exists at render time. Server components let you write fetch-and-render in one pass. For content-heavy pages, this genuinely reduces boilerplate.

Security benefits. Environment variables and API keys stay on the server. You can query a database or call a privileged API from a server component without any of that leaking to the client bundle. This was previously a footgun with plain SSR — it's now the default behavior.


What It Means for Architecture

RSC changes how you think about where logic lives.

Before: logic lives in hooks and components, state flows down, side effects are isolated to useEffect.

After: you have a new axis — server vs client — that cuts across your component tree. Data fetching and transformation happens on the server. Interactivity and user state happens on the client. The boundary between them is a first-class architectural concern.

In practice, this means you'll start thinking about which parts of a page are interactive (client) and which are static or data-driven (server). Most UI ends up being a mix — a server component renders the shell and passes data down to a client component that handles interaction.

// Server component fetches, client component handles interaction
async function ArticlePage({ slug }: { slug: string }) {
  const article = await getArticle(slug)
  return <ArticleViewer article={article} />
}

;('use client')
function ArticleViewer({ article }: { article: Article }) {
  const [liked, setLiked] = useState(false)
  // ...
}

This is a clean pattern once it becomes second nature. Getting there takes a few confusing hours of "why can't I use useState here?"


Should You Use It?

If you're starting a new Next.js project today, RSC is the default and you should lean into it rather than fight it. The mental model is unfamiliar at first, but it's not fundamentally more complex than what you were doing before — it just requires you to be more deliberate.

If you have an existing Next.js codebase on the Pages Router, migrating isn't urgent. The App Router is stable and has real benefits, but the migration cost is real too. Do it when you have a reason, not just because it's new.

The honest summary: RSC is a genuine improvement for most production apps. The learning curve is a few days of confusion, the footguns are manageable once you know them, and the benefits — smaller bundles, simpler data fetching, better security defaults — are worth it.

Just don't expect the tutorials to prepare you for the boundary errors.