React Server Components Explained Plainly
React Server Components (RSC) have been the most discussed and least understood feature in the React ecosystem. The documentation is improving, but there’s still a lot of confusion about what RSC actually does versus what people think it does. Let me try to explain it plainly.
The Core Idea
React Server Components are components that run on the server and send their rendered output to the client. They never run in the browser. They can’t use useState, useEffect, or any browser API. They can directly access databases, file systems, and server-side resources.
The key distinction: Server Components render to a special format (not HTML) that React on the client can efficiently merge into the existing component tree. This is different from traditional server-side rendering (SSR), where the entire page is rendered to HTML.
Server Components vs SSR
This is the most common source of confusion. SSR and Server Components solve different problems.
SSR renders your React components to HTML on the server, sends that HTML to the client, and then “hydrates” it — downloading all the component JavaScript and re-attaching event handlers. After hydration, everything runs on the client. SSR improves initial load time but doesn’t reduce the amount of JavaScript shipped.
Server Components never ship their JavaScript to the client. A Server Component that fetches data from a database, processes it, and renders a table sends only the rendered output — not the component code, not the database driver, not the data processing logic. The client JavaScript bundle is smaller because Server Components aren’t in it.
In practice, a Next.js page might use both: Server Components for static parts of the page (navigation, content display, data fetching) and Client Components for interactive parts (forms, buttons with click handlers, animations).
The “use client” Boundary
In Next.js App Router, every component is a Server Component by default. To make a component run on the client, you add "use client" at the top of the file:
"use client";
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
The "use client" directive creates a boundary. Everything below it (including imported modules) runs on the client. Everything above it runs on the server.
A common mistake is putting "use client" at the top of a page component because one small part of the page needs interactivity. Instead, extract the interactive part into a separate Client Component and import it into your Server Component:
// page.tsx (Server Component - no "use client")
import { Counter } from './Counter'; // Client Component
export default async function Page() {
const data = await fetchDataFromDB(); // runs on server
return (
<div>
<h1>{data.title}</h1>
<p>{data.content}</p>
<Counter /> {/* interactive part */}
</div>
);
}
The heading and paragraph render on the server. Only the Counter ships JavaScript to the client.
Data Fetching
Server Components make data fetching dramatically simpler. No useEffect, no loading states for initial data, no API routes to proxy database calls:
async function UserProfile({ userId }: { userId: string }) {
const user = await db.query('SELECT * FROM users WHERE id = ?', [userId]);
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
<p>Joined: {user.createdAt.toLocaleDateString()}</p>
</div>
);
}
This component runs on the server. It queries the database directly. No API endpoint needed, no client-side fetch, no loading spinner. The rendered HTML is sent to the client as part of the page.
This pattern works well for pages that display data. It works less well for pages that need real-time updates or client-side data manipulation.
Performance Implications
The performance benefits are real but nuanced:
Smaller bundles. Libraries used only in Server Components (database drivers, markdown parsers, date libraries) aren’t included in the client bundle. For data-heavy applications, this can reduce bundle size by 30-50%.
Faster initial render. Server Components stream their output to the client, meaning parts of the page can appear before the entire page is rendered. Combined with Suspense boundaries, you get progressive loading without writing loading state logic.
No waterfalls. Server Components can fetch data in parallel on the server without the sequential request waterfalls that plague client-side data fetching (component renders, triggers fetch, child component renders, triggers another fetch).
The Learning Curve
RSC introduces new mental models that take time to internalise:
- Components are server-first, not client-first
- Data fetching happens in components, not in hooks or API routes
- The boundary between server and client must be explicitly managed
- Passing data from Server to Client Components happens through props (which must be serialisable)
- You can’t pass functions or event handlers from Server to Client Components
These constraints feel limiting at first but lead to cleaner architecture once you internalise them. The server/client boundary forces you to separate data logic from interaction logic.
Should You Adopt RSC?
If you’re starting a new Next.js project, Server Components are the default in App Router. Learn them — they’re the future of the framework.
If you’re maintaining a Pages Router application, there’s no urgent need to migrate. Pages Router is stable and supported. Plan a migration when you have a natural opportunity (major feature addition, rewrite of a section).
If you’re not using Next.js, RSC support in other frameworks is still emerging. Remix, Gatsby, and other React frameworks are evaluating RSC integration, but the ecosystem outside Next.js isn’t mature yet.
React Server Components represent the biggest architectural shift in React since hooks. Take time to understand the model before forming an opinion. The initial confusion is normal — the long-term benefits are substantial.