| | |

React Server Components 02 🧬 The Mental Model — RSC vs. SSR vs. Client Components

⏱️ Estimated read: 12 min | |

The most common source of confusion in the React ecosystem is the conflation of three distinct concepts: Client-Side Rendering (CSR), Server-Side Rendering (SSR), and React Server Components (RSC). They are not competing alternatives; they are layers that solve different problems. The previous chapter established that RSC is a component type, not a framework. This chapter builds the mental model that separates the three, and explains why the “Client Component” name is misleading about where the code actually runs.

The distinction is not about where the HTML is generated. It is about where the component code runs and whether that code ships to the browser. Before RSC, every React component was a Client Component. It could be rendered to HTML on the server (SSR) or rendered in the browser (CSR), but the same JavaScript always shipped to the client to hydrate the page . RSC changes this by introducing a component type whose code never reaches the browser, regardless of whether the page is server-rendered or statically generated.

Key point: A Client Component is not “client-only.” It runs on the server during the initial render and again in the browser during hydration . A Server Component runs only on the server and its code is never hydrated. The word “Client” indicates that the component’s code also runs in the browser, not that it runs exclusively there.


Why the three concepts are separate

The CSR problem.

Client-Side Rendering sends an empty HTML shell and a large JavaScript bundle to the browser. React mounts the component tree in the browser, and the user sees a blank screen until the bundle downloads and executes . This is the model that made React feel slow on first load, and it is the model that RSC and SSR both aim to improve. But they improve it in different ways.

The SSR problem.

Server-Side Rendering generates the initial HTML on the server, so the browser paints content immediately. The server fetches the data and renders the tree before sending the first byte . This improves the First Contentful Paint, but it does not reduce the JavaScript bundle. The same component code that rendered the HTML must still ship to the browser to hydrate the page and attach the event listeners . The hydration step is the cost: the page is visible but not interactive until React reconciles the server HTML with the client tree.

The RSC problem.

RSC addresses the bundle size and the data-fetching waterfall, not the initial paint. A Server Component’s code stays on the server. It can query the database, read the filesystem, and access secrets without an API route, and none of that code is included in the client bundle . The component renders to a special format called the RSC Payload, which contains the rendered output and the serialized props for the Client Components . The browser never downloads the Server Component’s JavaScript.


a. The rendering environments

The component name suggests a clean split between server and browser, but both Client and Server Components render on the server. The difference is whether their code also runs in the browser .

Component TypeServer RenderBrowser Render
Server ComponentYesNo
Client ComponentYes (SSR/SSG)Yes (hydration)

A Client Component is server-rendered during the initial page load, and its HTML is part of the response. The browser then hydrates it by running the same component code and attaching the event listeners . A Server Component is server-rendered, but its code is not sent to the browser, so there is nothing to hydrate .

The 'use client' directive is the boundary marker. It does not mean “render only on the client.” It means “this module and its transitive dependencies belong to the client module graph, and their code will ship to the browser” . The absence of the directive means the component is a Server Component by default in frameworks like Next.js App Router .


b. The RSC Payload and the reconciliation

The server produces two outputs for a page: the HTML and the RSC Payload. The HTML is the initial visual, and the RSC Payload is the description of the component tree that React uses to reconcile the Client Components .

The RSC Payload contains three things: the rendered output of the Server Components, the placeholders for the Client Components, and the serialized props passed to those Client Components . The client receives the payload, downloads the JavaScript for the Client Components, and completes the rendering. The Server Components are already finished; their output is in the payload, and their code is not needed.

This is why the mental model must separate SSR from RSC. SSR is about producing HTML for the initial request. RSC is about keeping the Server Component code off the client. A page can use SSR without RSC (the traditional Next.js Pages Router model), and a page can use RSC without SSR (a client-side navigation after the initial load) . In practice, the Next.js App Router combines both: the Server Components are rendered on the server, the HTML is streamed, and the RSC Payload carries the tree to the client for the Client Components to hydrate.


c. The composition boundary

The most important rule of the mental model is that the 'use client' boundary is in the module graph, not the render tree . A Server Component can be passed as a children prop to a Client Component, and it remains a Server Component. The Client Component receives the already-rendered output, not the Server Component’s module, so the Server Component’s code does not enter the client bundle .

// Client Component: Modal.tsx
'use client'
import { useState } from 'react'

function Modal({ children }: { children: React.ReactNode }) {
  const [open, setOpen] = useState(false)
  return (
    <div>
      <button onClick={() => setOpen(!open)}>Toggle</button>
      {open && <div>{children}</div>}
    </div>
  )
}
// Server Component: page.tsx
import Modal from './Modal'
import Cart from './Cart' // Cart is a Server Component

function Page() {
  return (
    <Modal>
      <Cart /> // Rendered on the server, passed as children
    </Modal>
  )
}

The Cart is a Server Component, and it is passed as the children of the Modal . The Cart is rendered on the server, and its output is included in the RSC Payload. The Modal receives the rendered output, not the Cart module, so the Cart code never reaches the client. This is the composition pattern that lets the interactive chrome wrap the static content without pulling the server code into the client bundle.


Complete Example Session

// ============================================
// PART 1: THE SERVER COMPONENT (DEFAULT)
// ============================================
async function ProductPage({ id }: { id: string }) {
  const product = await db.products.find(id)
  return (
    <div>
      <h1>{product.name}</h1>
      <AddToCartButton productId={id} />
    </div>
  )
}

// ============================================
// PART 2: THE CLIENT COMPONENT
// ============================================
'use client'
function AddToCartButton({ productId }: { productId: string }) {
  const [added, setAdded] = useState(false)
  return (
    <button onClick={() => { setAdded(true); addToCart(productId) }}>
      {added ? 'Added' : 'Add to Cart'}
    </button>
  )
}

// ============================================
// PART 3: THE SSR RENDER (INITIAL HTML)
// ============================================
// The server renders ProductPage to HTML.
// The AddToCartButton is also rendered to HTML.
// The HTML includes the button with "Add to Cart".

// ============================================
// PART 4: THE RSC PAYLOAD
// ============================================
// The payload contains:
// - The rendered h1 (from ProductPage)
// - A reference to the AddToCartButton client module
// - The serialized productId prop

// ============================================
// PART 5: THE HYDRATION
// ============================================
// The browser downloads the AddToCartButton JS.
// React hydrates the button, attaching the onClick handler.
// The ProductPage code is never downloaded.

// ============================================
// PART 6: THE COMPOSITION
// ============================================
// ProductPage (Server) renders AddToCartButton (Client).
// The productId is serializable, so it crosses the boundary.
// The ProductPage code stays on the server.

The six parts demonstrate the mental model: the Server Component with data access, the Client Component with interactivity, the SSR HTML, the RSC Payload, the hydration, and the composition.


Quick Reference

RSC vs SSR

AspectSSRRSC
What it producesHTML for the initial requestRSC Payload for the component tree
Component code on clientAll components shipOnly Client Components ship
HydrationAll Client ComponentsOnly Client Components
Data fetchingBefore render (props) or during renderDuring render (Server Components)
Bundle size reductionNoneSignificant

Component Types

TypeDirectiveRuns on ServerRuns in Browser
Server ComponentNoneYesNo
Client Component'use client'Yes (SSR)Yes (hydration)

The 'use client' Boundary

RuleDetail
PlacementTop of file, before imports
ScopeModule graph, not render tree
TransitiveAll imports become Client Components
Server as childrenStays server-rendered

The RSC Payload

ContentPurpose
Rendered Server outputThe static HTML-like result
Client Component placeholdersWhere the client renders
Serialized propsThe data crossing the boundary

Best Practices

✅ Do This:

// Start with the Server Components
async function Page() { ... }                                 # ✅

// Add the 'use client' only where the interactivity is needed
'use client'                                                  # ✅

// Pass the Server Components as children
<ClientWrapper><ServerComponent /></ClientWrapper>            # ✅

// Keep the client leaves small
<AddToCartButton />                                           # ✅

// Pass the serializable data only
<Client likes={post.likes} />                                 # ✅

❌ Don’t Do This:

// Add the 'use client' to the static components
'use client'    // unnecessary                                # ❌

// Assume the Client Component is client-only
// It is server-rendered during SSR                           # ❌

// Import the Server Component into the Client Component
import { ServerComp } from './server'    // Becomes client     # ❌

// Pass the functions as the props
<Client onClick={() => {}} />    // Not serializable           # ❌

// Confuse the 'use server' with the Server Component
'use server'    // This is for Server Functions              # ❌

Common Pitfalls

PitfallWhy It HappensFix
The whole page is client-side'use client' at the topMove it to the interactive leaf
The Server Component becomes clientImported into a Client ComponentPass it as children
The serialization errorA function propUse a Server Function or remove it
The hydration mismatchThe different output on server and clientCheck the conditional rendering
The “Client Component” confusionAssumed client-onlyIt runs on the server during SSR

Real-World Examples

1. Server Component with data

async function Page() {
  const data = await db.query(...)
  return <List data={data} />
}

2. Client Component with state

'use client'
function Counter() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(count + 1)}>{count}</button>
}

3. Client wraps Server children

<Modal><Cart /></Modal>

4. The RSC Payload

The payload contains:
- The rendered Server Component output
- The Client Component placeholders
- The serialized props

5. The SSR HTML

The initial response includes:
- The rendered HTML for all components
- The RSC Payload for the Client Components

6. The hydration

The browser downloads the Client Component JS.
React hydrates the Client Components.
The Server Components are already finished.

7. The module graph split

Server graph: Page, db
Client graph: Counter, useState
The two graphs are separate.

8. The serialization check

<Client data={{ id: 1, name: 'Test' }} />  // ✅ plain object
<Client onClick={() => {}} />              // ❌ function

9. The async component

async function Page() {
  const data = await fetch(...)
  return <div>{data}</div>
}

10. The context provider

'use client'
function ThemeProvider({ children }) {
  return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}

Visual

The Three Layers

┌──────────────────────────────────────────────────────────────┐
│  CSR, SSR, AND RSC ARE SEPARATE LAYERS                       │
│                                                              │
│  CSR:                                                        │
│  ┌────────────────────────────────────────────────────────┐  │
│  │ Empty HTML + JS bundle → React renders in the browser  │  │
│  │ The component code runs only in the browser            │  │
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
│  SSR:                                                        │
│  ┌────────────────────────────────────────────────────────┐  │
│  │ HTML + JS bundle → React hydrates in the browser       │  │
│  │ The component code runs on the server AND in the browser│ │
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
│  RSC:                                                        │
│  ┌────────────────────────────────────────────────────────┐  │
│  │ RSC Payload + Client JS → Client Components hydrate    │  │
│  │ The Server Component code runs only on the server      │  │
│  └────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────┘

The Client Component Is Not Client-Only

┌──────────────────────────────────────────────────────────────┐
│  A CLIENT COMPONENT RUNS ON THE SERVER DURING SSR            │
│                                                              │
│  Direct visit:                                               │
│  ┌────────────────────────────────────────────────────────┐  │
│  │ Server: renders the Client Component to HTML           │  │
│  │ Browser: hydrates the Client Component                 │  │
│  │ The component code runs twice                          │  │
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
│  Client-side navigation:                                     │
│  ┌────────────────────────────────────────────────────────┐  │
│  │ Server: sends the RSC Payload                          │  │
│  │ Browser: renders the Client Component without SSR HTML │  │
│  └────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────┘

The Module Graph Boundary

┌──────────────────────────────────────────────────────────────┐
│  THE BOUNDARY IS IN THE MODULE GRAPH                         │
│                                                              │
│  Server module graph:                                        │
│  ├── app/page.tsx (Server)                                   │
│  ├── app/ui/cart.tsx (Server)                                │
│  └── app/ui/modal.tsx (Client)  ← the boundary               │
│                                                              │
│  Client module graph:                                        │
│  ├── app/ui/modal.tsx (Client)                               │
│  └── (its imports)                                           │
│                                                              │
│  The Cart is passed as children to the Modal.                │
│  The Cart module is not in the client graph.                 │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
CSREmpty HTML + JS, renders in the browser
SSRHTML + JS, hydrates in the browser
RSCPayload + Client JS, Server Components stay on the server
Server ComponentNo directive, runs only on server
Client Component'use client', runs on server (SSR) and browser
'use client' boundaryModule graph, not render tree
RSC PayloadRendered Server output + Client placeholders + props
Server as childrenStays server-rendered, not in client bundle

Key takeaways:

  • CSR, SSR, and RSC are separate layers. CSR renders in the browser, SSR renders HTML on the server but ships all the JavaScript, and RSC keeps the Server Component code on the server and ships only the Client Component code .
  • A Client Component is not client-only. It runs on the server during SSR and again in the browser during hydration. The “Client” name means the code also runs in the browser, not that it runs exclusively there .
  • The 'use client' boundary is in the module graph. A Server Component passed as a children prop to a Client Component remains a Server Component. The Client Component receives the rendered output, not the module .
  • The RSC Payload is the transport. It contains the rendered Server Component output, the Client Component placeholders, and the serialized props. The browser uses it to reconcile the Client Components .
  • RSC reduces the bundle size, SSR improves the initial paint. They solve different problems and are often combined. The Next.js App Router uses both: the Server Components render on the server, the HTML is streamed, and the RSC Payload carries the tree to the client .
  • The serialization rules apply only to the boundary. The props passed from a Server Component to a Client Component must be serializable. The Server Component’s internal logic and data access are not constrained .

Remember: The mental model separates three things: where the HTML is produced (CSR vs SSR), where the component code runs (Server vs Client), and whether the code ships to the browser (RSC vs traditional). A Client Component is server-rendered during SSR, but its code also ships and hydrates. A Server Component is never shipped and never hydrated. The boundary is in the module graph, not the render tree. The result is a model where the server handles the data and the static rendering, and the client handles the interactivity, without the server code ever reaching the browser.


Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!