React Server Components: What They Are and What They Actually Change

Rendering & Frameworks

React Server Components (RSCs) introduced a new component type that runs on the server only and is never sent to the browser as JavaScript. They generate a serialized component tree (not HTML) that is streamed to the client and reconstituted by the React runtime — without requiring a bundle download for the component code itself.

This is fundamentally different from Server-Side Rendering, which renders HTML on the server but still ships all component JavaScript to the browser.

The Old Model

In SSR (Pages Router):

  1. Server renders the full component tree to HTML
  2. Browser receives the HTML and shows it
  3. Browser downloads the full JavaScript bundle
  4. React hydrates: attaches event handlers and makes the page interactive

The component code is executed twice: once on the server (to generate HTML) and once on the client (to hydrate). The full component tree — including components that have no interactivity — ships in the bundle.

The Server Component Model

In the App Router:

  • Server Components run on the server only and generate UI. They can fetch data directly (database queries, filesystem reads). Their code is never included in the browser bundle.
  • Client Components ('use client') run in the browser (and optionally on the server for SSR). They handle interactivity, state, and browser APIs.
// UserProfile.tsx — Server Component (no directive)
// This code is NEVER sent to the browser
import { db } from '@/lib/db'

export async function UserProfile({ userId }) {
  const user = await db.users.findById(userId) // direct DB access

  return (
    <div>
      <h1>{user.name}</h1>
      <LikeButton /> {/* Client Component — interactive */}
    </div>
  )
}
// LikeButton.tsx — Client Component
'use client'
import { useState } from 'react'

export function LikeButton() {
  const [liked, setLiked] = useState(false)
  return <button onClick={() => setLiked(!liked)}>{liked ? '❤️' : '🤍'}</button>
}

LikeButton ships to the browser with its JavaScript. UserProfile does not.

What Server Components Enable

Direct data access: No API layer needed. A Server Component can query a database, read a file, or call internal services directly. The result is embedded in the rendered tree without exposing credentials or internal queries to the client.

Zero bundle cost: A component that renders a complex Markdown document using a 100KB parsing library can use that library server-side with zero client bundle impact. The user receives the rendered output, not the library.

Automatic request deduplication: React's cache() function (added alongside RSC) deduplicates async calls within a single render pass. Two components that fetch the same user data will share one database call.

Streaming by default: Server Components integrate natively with Suspense streaming. Components that fetch slow data can be wrapped in Suspense and stream in independently.

What Server Components Cannot Do

Server Components cannot use:

  • useState, useReducer, useEffect, useContext
  • Event handlers (onClick, onChange)
  • Browser-only APIs (window, document, localStorage)
  • Class components

These capabilities belong exclusively to Client Components. The boundary is declared by adding 'use client' at the top of a file.

The Component Tree Model

Server and Client Components compose: Server Components can render Client Components (they pass the rendered RSC payload to the client, which includes the Client Component references). Client Components cannot render Server Components — but they can render children that were passed to them as props from a Server Component parent.

// Server Component passes a Server Component as a child
export function Layout({ children }) {
  return <ClientShell>{children}</ClientShell>
}

// ClientShell.tsx — 'use client'
// children prop here is the already-rendered Server Component output

This pattern is how you put a Client Component (the interactive shell) inside a Server Component (the layout) without the shell needing to know how its children were rendered.

Server Actions

Server Components introduced the concept of Server Actions: functions marked 'use server' that run on the server but can be called from the client. They are the primary mutation mechanism in the App Router:

// form-actions.ts
'use server'
export async function createPost(formData: FormData) {
  const title = formData.get('title') as string
  await db.posts.create({ title })
  revalidatePath('/posts')
}

// CreatePost.tsx — Client Component
import { createPost } from './form-actions'
<form action={createPost}>
  <input name="title" />
  <button type="submit">Create</button>
</form>

No API route, no fetch, no JSON serialization. The form posts directly to a server function.

Conclusion

React Server Components shift the default from "run on both, send everything to the client" to "run on server only unless you explicitly opt into client behavior." The result is smaller bundles, simpler data fetching, and a cleaner separation between interactive and non-interactive UI. The new mental model — understanding which components are which, and where the 'use client' boundary should be placed — is the main learning curve, but the payoff in performance and architecture clarity is substantial.