React Server Components: The Complete Conceptual Guide
AI generated
</>
{ }
React · RSC · Next.js · Streaming · App Router
React Server Components:
the complete conceptual guide

React Server Components are not a new rendering model, they are a fundamental paradigm shift. Thinking in client-only components no longer works. Whoever truly understands RSC builds apps that ship less JavaScript, load faster and can encapsulate database access directly inside components.

20 min read RSC · Client Components · Streaming · Suspense · App Router · Compositing React 18+ · Next.js 14+ · TypeScript

1. The new paradigm: why RSC exist

The core problem that React Server Components solve can be described in one sentence: too much JavaScript is shipped to the client that it does not even need. A component that renders database data and has no interactivity does not need to run in the browser. Classic SSR renders the page to HTML on the server, but still ships the full JavaScript bundle so React can "hydrate" it in the browser, meaning the entire component logic ends up in the client bundle even though many components never become interactive in the browser.

RSC take a different path: Server Components run exclusively on the server. Their code never ends up in the JavaScript bundle sent to the browser. Libraries that are only used by Server Components cost no bundle space at all. Database access, file system operations and API calls can happen directly inside components, without useEffect, without useState, without loading states for the initial data fetch. This significantly reduces complexity and improves performance through smaller bundles and faster initial render times.

2. RSC architecture: server, client and the boundary between them

In an RSC architecture there are two worlds: the server world and the client world. The boundary between them is explicitly marked and one-way permeable. All components are Server Components by default, they run on the server, render to a serialized component description (not to HTML) and are transferred to the client. Client Components must be explicitly marked with the 'use client' directive.

The boundary is one-directional: Server Components can import Client Components and render them as children. But Client Components cannot import Server Components, that would mean pulling server-only code into the client bundle. There is, however, a way out: Server Components can be passed to Client Components as a children prop. The Client Component element then receives the already-rendered RSC output as a prop, without importing the server code itself. This compositing pattern is the heart of the RSC architecture.


// app/page.tsx - Server Component (default, no directive needed)
// Direct database access - no useEffect, no loading state needed
import { db } from '@/lib/db';
import { ProductCard } from './ProductCard'; // Client Component
import { Suspense } from 'react';
import { ProductSkeleton } from './ProductSkeleton';

// Server Component: runs only on server, never in browser bundle
export default async function ProductsPage() {
  // Direct DB call - no API round-trip, no client bundle cost
  const products = await db.product.findMany({
    where: { active: true },
    orderBy: { createdAt: 'desc' },
    take: 20,
  });

  return (
    <main>
      <h1>Products</h1>
      <Suspense fallback={<ProductSkeleton count={20} />}>
        {/* Pass server-fetched data as props to client component */}
        <ProductGrid products={products} />
      </Suspense>
    </main>
  );
}

// app/ProductGrid.tsx - Server Component renders Client Components
async function ProductGrid({ products }: { products: Product[] }) {
  return (
    <div className="grid grid-cols-3 gap-4">
      {products.map((product) => (
        // ProductCard is 'use client' - handles add-to-cart interaction
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

3. Server Components: what they can and cannot do

Server Components can do everything possible on the server: database access with ORMs, reading the file system, calling backend APIs directly, reading secrets from environment variables. They can be asynchronous, async function ServerComponent() is a fully supported pattern. This means data can be fetched directly in the component body with await, without useEffect or useState for the loading state.

What Server Components cannot do: they must not hold browser-specific state (useState, useReducer), register effects (useEffect), use event handlers (onClick, onChange) or use browser APIs such as window, localStorage or document. Attempting to use these in a Server Component leads to a build error. This is intentional: Server Components are pure render machines without side effects on the client side.

4. Client Components: use client and what it means

'use client' is not a statement about where a component runs, it is a boundary marker. A Client Component runs both on the server (for the initial SSR HTML) and in the browser. The directive tells React "from here on the client graph begins." All imports of a 'use client' file are automatically included in the client bundle, even if they have no 'use client' directive of their own.

This is a critical consequence: if a Client Component imports a large library, that library ends up fully in the bundle. In Server Components the same library would cost no bundle space. So the strategic question is: which parts of the app really need interactivity? Everything else should stay a Server Component. A good rule of thumb: set the 'use client' boundary as deep as possible in the component tree, that way most of the app stays in the server graph and ships minimal JavaScript.


// Compositing pattern: Server passes pre-rendered RSC as children to Client
// This avoids importing server code into client bundle

// app/Dashboard.tsx - Server Component (no directive)
import { SidebarNav } from './SidebarNav'; // Client Component (needs state)
import { Analytics } from './Analytics';    // Server Component (DB access)

export default async function Dashboard() {
  const stats = await fetchStats(); // Direct server-side call

  // Pass Server Component as children to Client wrapper
  return (
    <SidebarNav>
      {/* Analytics is a Server Component passed as prop - not imported by client */}
      <Analytics stats={stats} />
    </SidebarNav>
  );
}

// app/SidebarNav.tsx - Client Component wrapper
'use client';
import { useState } from 'react';

export function SidebarNav({ children }: { children: React.ReactNode }) {
  const [collapsed, setCollapsed] = useState(false);

  return (
    <div className={`layout ${collapsed ? 'sidebar-collapsed' : ''}`}>
      <nav>
        <button onClick={() => setCollapsed(c => !c)}>Toggle</button>
        {/* Nav items */}
      </nav>
      <main>{children}</main>
      {/* children is pre-rendered RSC output - no server code in bundle */}
    </div>
  );
}

5. Compositing patterns: nesting server and client correctly

The compositing pattern is the most important technique for a clean RSC architecture. The basic pattern: interactive wrapper components are Client Components, their content can be Server Components passed in as a children prop. This allows an interactive accordion wrapper to run on the client while its entire content, text, images, product data, is rendered as a Server Component and carries no bundle weight.

Another important compositing pattern is the leaf component pattern: pushing interactivity down to the leaves of the component tree. Instead of turning an entire product page into a Client Component, the product page stays a Server Component. Only the "add to cart" button is a small, self-contained Client Component. This drastically minimizes the client bundle share. Context providers are typically Client Components sitting at the top of the tree to provide state to child Client Components, but they can still render Server Component children.

6. Streaming with Suspense: progressive render delivery

Streaming in RSC means that the server does not wait for the slowest data source before it starts sending. Instead, it sends the HTML shell immediately and streams parts of the page as they become ready. The browser can build up the page progressively, the user sees a shell immediately and content appears as soon as its data is available. This significantly improves perceived load time, even if the total load time stays the same.

The technical vehicle for this is Suspense. A Server Component that is waiting for data can be wrapped in <Suspense fallback={...}>. React streams the fallback content (skeleton, spinner) immediately and replaces it with the real content as soon as the Server Component is done. Multiple parallel Suspense boundaries enable independent streaming of different page areas, the sidebar can still be loading while the header is already visible and the product area is still waiting.

7. Data access directly in Server Components

One of the most transformative aspects of RSC is direct database access inside components. What previously required either an API route (an HTTP request from the client) or complex SSR data fetching in getServerSideProps can now live directly in the component body. An ORM such as Prisma or Drizzle can be imported directly, the query runs server-side, and the result is passed down as props to child components.

This has deep implications for architecture: data fetching now lives closer to where the data is actually needed. Instead of props drilling from one top-level data fetch through many layers, every Server Component can fetch its own data. React guarantees that parallel fetches run at the same time, the Promise.all approach is already built in. For caching and deduplication across multiple components that need the same data, Next.js provides fetch memoization.

8. Server Actions: forms and mutations

Server Actions are the other side of the RSC coin: if Server Components simplify reading, Server Actions simplify writing. A Server Action is a function with the 'use server' directive that can be called from the client but runs on the server. It can be wired directly into a <form action={...}> and handles the entire HTTP handling itself.

The progressive enhancement principle is built into Server Actions: a form with a Server Action still works without JavaScript in the browser, because it functions as a normal HTML form with a POST request. With JavaScript, the data is transmitted through a fetch-like mechanism, without a page reload. Server Actions can validate, perform database operations and revalidate the Next.js cache, all on the server, without writing an API route. This eliminates an entire category of boilerplate code.

9. RSC vs. classic SSR compared

RSC and classic SSR solve similar problems in fundamentally different ways. The details of the differences decide which model is the better fit for which use case.

Aspect Classic SSR (Pages Router) React Server Components (App Router)
Data fetching getServerSideProps at page level Directly in every component with async/await
JavaScript bundle All components in the bundle Server Components add no bundle weight
Streaming No (all or nothing) Yes, with Suspense boundaries
Form handling API route + fetch on the client Server Actions directly in forms
Learning curve Familiar, clear-cut model New concepts: boundary, compositing

Classic SSR remains valid for projects that already run on the Pages Router and where a migration is not justified. RSC pays off particularly for data-heavy apps where bundle size and time-to-first-byte are critical, e-commerce, content platforms and dashboards benefit the most. The App Router is not "better SSR", it is a different mental model that requires a learning phase.

Mironsoft

React Server Components · Next.js App Router · Migration · Architecture

Migrating to React Server Components?

We plan and implement the migration from the Pages Router to the App Router, with a correct server/client boundary, compositing patterns and streaming architecture for maximum performance.

RSC architecture

Set the server/client boundary optimally and implement compositing patterns

Streaming setup

Suspense boundaries and progressive render delivery for better UX

Migration

Migrate from Pages Router to App Router step by step without downtime

10. Summary

React Server Components are not an incremental update, they are a new mental model. Server Components run exclusively on the server, have access to backend resources and carry no JavaScript bundle weight. Client Components are the interactive parts that run in the browser and are marked with 'use client'. The server/client boundary is bridged through compositing patterns: Server Components pass their rendered output as a children prop to Client wrappers.

Streaming with Suspense delivers page sections progressively as soon as their data is ready, without waiting for the slowest data source. Server Actions simplify mutations and form handling by making server-side functions directly callable from the client. The result: less JavaScript, faster initial render times, simpler data fetching and a clearer architectural model for complex, data-heavy React apps.

React Server Components: the essentials at a glance

Server Components

The default. No bundle share, direct DB access, async/await in the body. No useState, useEffect or event handlers.

Client Components

'use client' marks the boundary. Interactivity, state, effects. Set the boundary as deep as possible for a minimal bundle.

Compositing

Pass Server Components as children to Client wrappers. The client never imports server code. Leaf components for interactivity.

Streaming

Suspense boundaries enable progressive delivery. Skeleton visible immediately, content streams in after. Parallel boundaries run independently.

11. FAQ: React Server Components

1Server Components vs. SSR?
SSR still sends the full JS bundle. Server Components never end up in the bundle, no JavaScript weight for pure render components.
2useState in a Server Component?
Not allowed. Server Components must not use client state, effects or event handlers. For that you need 'use client'.
3What does 'use client' mean?
A boundary marker for the client graph. All imports of this file end up in the bundle. Set the boundary as deep as possible.
4Client renders a Server Component?
Not directly. Pass Server Components as a children prop instead. The client receives the rendered RSC output, importing no server code.
5What are Server Actions?
'use server' functions that are callable from the client, running on the server. No API route needed. Progressive enhancement built in.
6What is streaming in RSC?
The server sends the shell immediately, streams content in as data becomes ready. Suspense boundaries control fallback vs. content. Parallel boundaries run independently.
7DB access in Server Components?
Yes. Import Prisma/Drizzle directly, run the query with await in the component body. No API layer, no useEffect. DB code never in the bundle.
8RSC only with Next.js?
No, RSC is a React feature. The Next.js App Router is currently the most mature implementation. Other frameworks are following.
9Caching with Server Components?
Next.js: fetch memoization, Data Cache, Full Route Cache. revalidatePath and revalidateTag for targeted invalidation after mutations.
10Migrate Pages Router to App Router?
Incrementally: app/ alongside pages/. Migrate routes step by step. Replace getServerSideProps with async Server Components. Mark interactivity with 'use client'.