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.
Table of Contents
- 1. The new paradigm: why RSC exist
- 2. RSC architecture: server, client and the boundary between them
- 3. Server Components: what they can and cannot do
- 4. Client Components: use client and what it means
- 5. Compositing patterns: nesting server and client correctly
- 6. Streaming with Suspense: progressive render delivery
- 7. Data access directly in Server Components
- 8. Server Actions: forms and mutations
- 9. RSC vs. classic SSR compared
- 10. Summary
- 11. FAQ
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.