HomeAboutProjectsExperienceSpotlightEssaysEducation
← All essays

Understanding React Server Components

6 min read
reactnextjsarchitecture

React Server Components (RSC) are one of those features that sounds abstract until it clicks — and once it does, you start seeing your entire component tree differently.

The Core Idea

Before RSC, every React component ran in the browser. The server might render an initial HTML snapshot, but the JavaScript for all your components was shipped to the client and re-executed there.

RSC introduces a new category: components that only ever run on the server. They never ship their code to the browser. They can't use useState, useEffect, or any browser API — but they can do things client components can't:

  • Read directly from a database
  • Access the filesystem
  • Use secrets without exposing them
  • Import heavy libraries without adding to your bundle
// This component never reaches the browser
// The DB query, the import — none of it ships to the client
import { db } from "@/lib/db";
import { marked } from "marked"; // heavy library, zero bundle cost

export default async function ArticlePage({ slug }: { slug: string }) {
  const article = await db.articles.findOne({ slug });
  const html = marked(article.content);

  return <article dangerouslySetInnerHTML={{ __html: html }} />;
}

The Mental Model

Think of your component tree as having two zones:

Server Zone                  │  Client Zone
─────────────────────────────│──────────────────────
Layout                       │
  └─ ArticlePage (async!)    │
       └─ TableOfContents    │
            └─ ─ ─ ─ ─ ─ ─ ─│─ ▶ InteractiveDemo ("use client")
                             │       └─ CodeBlock
                             │       └─ CopyButton

Server components can render client components. But client components cannot render server components (they've already left the server by that point).

When to Use Which

Default to Server Components for:

  • Pages and layouts
  • Data fetching
  • Static content rendering
  • Anything that reads from a DB or API

Opt into Client Components ("use client") for:

  • Interactivity (onClick, forms, toggles)
  • Browser APIs (localStorage, window)
  • React hooks (useState, useEffect, useContext)

The Gotcha Everyone Hits

You can't pass non-serializable props from a server component to a client component. Functions, class instances, and complex objects won't cross the boundary.

// ❌ This breaks — functions aren't serializable
<ClientComponent onClick={() => doSomething()} />

// ✅ This works — define the handler inside the client component
<ClientComponent itemId={item.id} />

Why This Matters for Performance

The real win is bundle size. A markdown parser, a date formatting library, a syntax highlighter — if they only live in server components, they contribute zero bytes to your JavaScript bundle.

For content-heavy sites, this is a substantial difference in load time, especially on mobile.


RSC is a shift in how you think about components, not just an optimization. Once you internalize the server/client boundary, the architecture of a Next.js app starts feeling much more intentional.