What are React Server Components?
What are React Server Components?
Section titled “What are React Server Components?”Introduction
Section titled “Introduction”React Server Components (RSC) are a new type of React component that render exclusively on the server. Unlike traditional React components that run in the browser, Server Components execute on the server, generate HTML, and produce a special RSC payload that React uses to efficiently update the client-side component tree. They are the default component type in the Next.js App Router.
Why do we need this?
Section titled “Why do we need this?”Traditional React components execute entirely in the browser. This means:
- Large bundle sizes — Every component’s JavaScript must be downloaded and parsed
- No direct data access — Components can’t directly access databases; you need API routes
- Waterfall requests — Components fetch data → render → hydrate → fetch more data
- Slower initial loads — Everything waits for JavaScript to download and execute
Server Components solve all of these problems by running on the server, sending only the rendered output to the client.
Problem Statement
Section titled “Problem Statement”React applications have traditionally shipped all component code to the browser. A page that renders a blog post from a database needs: the blog post component code, the markdown parser, the date formatter library, and the data fetching logic — all downloaded as JavaScript. This creates a poor user experience, especially on slow connections or low-powered devices.
Real World Story
Section titled “Real World Story”Spotify’s web player historically struggled with slow initial load times because their React application shipped large bundles of JavaScript for features like audio processing, playlist management, and data visualization. By adopting Server Components (via Next.js), they moved data fetching and initial rendering to the server, reducing the initial bundle size by 40% and improving Time to Interactive by 60%.
Real World Analogy
Section titled “Real World Analogy”Think of Server Components vs Client Components like restaurant kitchen vs dining table:
-
Server Components (kitchen): The chef prepares your meal (renders HTML), plates it (generates RSC payload), and serves it to you. You don’t see the cooking process (database queries, API calls), and you don’t need the kitchen utensils (npm dependencies) at your table.
-
Client Components (dining table): The salt shaker (interactive button), menu (navigation), and water glass (form inputs) are things you interact with directly — these stay on the table (browser).
The kitchen handles all the complex preparation work; the table only has what you need to interact with.
Visual Explanation
Section titled “Visual Explanation”Server Components vs Client Components:
┌─────────────────────────────────────────────┐│ SERVER ││ ┌─────────────────────────────────────┐ ││ │ Server Component │ ││ │ - Fetches data from DB │ ││ │ - Renders markdown to HTML │ ││ │ - Formats dates │ ││ │ - Returns: <article>...</article> │ ││ └─────────────────────────────────────┘ ││ │ ││ ▼ HTML + RSC Payload │└─────────────────────────────────────────────┘ │ ▼┌─────────────────────────────────────────────┐│ CLIENT ││ ┌─────────────────────────────────────┐ ││ │ Client Component │ ││ │ - <button> onClick → like post │ ││ │ - <input> onChange → search │ ││ │ - <div> useState → toggle menu │ ││ └─────────────────────────────────────┘ │└─────────────────────────────────────────────┘Mermaid Diagram 1: Server Component Architecture
Section titled “Mermaid Diagram 1: Server Component Architecture”flowchart TD subgraph "Server" SC["Server Component"] --> F["Fetch data<br/>from DB / API"] F --> R["Render to<br/>HTML + RSC"] R --> P["Produce RSC Payload"] end
subgraph "Client" P --> C["Client receives<br/>HTML + RSC payload"] C --> H["Hydrate only<br/>Client Components"] H --> I["Interactive UI"] end
style SC fill:#7c3aed,color:#fff style P fill:#f59e0b,color:#000 style H fill:#22c55e,color:#fffInternal Working
Section titled “Internal Working”When Next.js renders a page with Server Components:
- Component tree resolution — Next.js traverses the component tree from the root layout
- Server Component execution — All Server Components execute on the server, including their async data fetching
- RSC Payload generation — A special serialized format (RSC Payload) is generated, containing the rendered component tree
- HTML generation — Server Components render to static HTML for initial page load (SEO, fast FCP)
- Streaming — HTML and RSC payload are streamed to the client as they become ready
- Client reconciliation — React uses the RSC payload to reconcile the component tree without sending Server Component code to the browser
Mermaid Diagram 2: Server Component Request Lifecycle
Section titled “Mermaid Diagram 2: Server Component Request Lifecycle”sequenceDiagram participant B as Browser participant N as Next.js Server participant SC as Server Component participant DB as Database
B->>N: GET /blog/post-1 N->>SC: Execute component tree SC->>DB: Query posts table DB-->>SC: Return post data SC->>SC: Render with markdown parser SC-->>N: HTML + RSC Payload N-->>B: Stream HTML + RSC B->>B: React reconciles tree Note over B: No Server Component JS<br/>sent to client!Architecture
Section titled “Architecture”The RSC architecture creates a clear server/client boundary:
flowchart TD subgraph "Server Component Tree" A["RootLayout (Server)"] --> B["Nav (Client)"] A --> C["Page (Server)"] C --> D["BlogList (Server)"] D --> E["BlogCard (Server)"] D --> F["LikeButton (Client)"] C --> G["Sidebar (Server)"] G --> H["Search (Client)"] end
subgraph "What ships to client" JS1["Nav JS ✅"] JS2["LikeButton JS ✅"] JS3["Search JS ✅"] NONE["RootLayout ❌<br/>Page ❌<br/>BlogList ❌<br/>BlogCard ❌<br/>Sidebar ❌"] end
style NONE fill:#f59e0b,color:#000 style JS1 fill:#22c55e,color:#fff style JS2 fill:#22c55e,color:#fff style JS3 fill:#22c55e,color:#fffMermaid Diagram 4: RSC Payload Example
Section titled “Mermaid Diagram 4: RSC Payload Example”flowchart LR subgraph "RSC Payload (serialized)" P1["$R = {<br/> type: 'div',<br/> props: { className: 'post' },<br/> children: [<br/> { type: 'h1', children: 'Hello World' },<br/> { ref: 1 } // reference to Client Component<br/> ]<br/>}"] end
subgraph "Client Bundle" P2["Client Component<br/>exports LikeButton<br/>sent as JS bundle"] end
P1 --> C["React reconciles<br/>on client"] P2 --> C
style P1 fill:#7c3aed,color:#fff style P2 fill:#f59e0b,color:#000Step-by-Step Flow
Section titled “Step-by-Step Flow”- Create a component — Export a function from a file WITHOUT
"use client"directive - Make it async — Add
asynckeyword to fetch data directly (database, API, file system) - Render on server — The component runs on the server during request/build time
- Produce output — Server Component generates HTML and RSC Payload (no JS sent to client)
- Stream to client — HTML for initial paint, RSC payload for React reconciliation
Syntax
Section titled “Syntax”// Server Component (default — no directive needed)// This component runs ONLY on the serverexport default async function BlogPost({ id }: { id: string }) { // Direct database access — no API route needed const post = await db.post.findUnique({ where: { id } })
// Server-side libraries — not sent to client const html = await markdownToHtml(post.content)
return ( <article> <h1>{post.title}</h1> <div dangerouslySetInnerHTML={{ __html: html }} /> <p>Published: {formatDate(post.createdAt)}</p> </article> )}Key differences from Client Components:
- No
"use client"directive - Can be
async(fetch data directly) - Cannot use hooks (
useState,useEffect, etc.) - Cannot use browser APIs
- Direct database and file system access
Basic Example
Section titled “Basic Example”A simple Server Component that fetches and renders data:
// app/page.tsx — Server Component by defaultinterface User { id: number name: string email: string}
async function getUsers(): Promise<User[]> { // Server Component can fetch data directly const res = await fetch('https://jsonplaceholder.typicode.com/users') return res.json()}
export default async function HomePage() { const users = await getUsers()
return ( <main> <h1>Users</h1> <ul> {users.map(user => ( <li key={user.id}> <strong>{user.name}</strong> — {user.email} </li> ))} </ul> </main> )}What’s happening:
- This component has NO
"use client"directive — it’s a Server Component getUsers()fetches data directly — no API route needed- The
asynckeyword lets us await data directly in the component - Zero JavaScript is sent to the client for this component
- The rendered HTML is streamed to the browser
Intermediate Example
Section titled “Intermediate Example”Server Component with error handling, empty states, and client component integration:
// app/posts/page.tsx — Server Componentimport { notFound } from 'next/navigation'import { PostCard } from './PostCard'import { SearchPosts } from './SearchPosts'
interface Post { id: string title: string excerpt: string publishedAt: string}
async function getPosts(): Promise<Post[]> { try { const res = await fetch('https://api.example.com/posts', { next: { revalidate: 60 }, // ISR — revalidate every 60 seconds })
if (!res.ok) { throw new Error(`Failed to fetch: ${res.status}`) }
return res.json() } catch (error) { console.error('Failed to fetch posts:', error) throw error // Let error.tsx handle it }}
export default async function PostsPage() { const posts = await getPosts()
if (posts.length === 0) { return ( <div className="empty-state"> <h2>No posts yet</h2> <p>Check back later for new content.</p> </div> ) }
return ( <div> <h1>Blog Posts</h1> {/* Client Component with interactivity */} <SearchPosts />
<div className="posts-grid"> {posts.map(post => ( // Server-rendered PostCard (data is fetched on server) <PostCard key={post.id} post={post} /> ))} </div> </div> )}// app/posts/SearchPosts.tsx — Client Component'use client'
import { useState } from 'react'
export function SearchPosts() { const [query, setQuery] = useState('')
return ( <input type="search" placeholder="Search posts..." value={query} onChange={(e) => setQuery(e.target.value)} className="search-input" /> )}What’s happening:
PostsPageis a Server Component that fetches data and renders the page- Error handling with
try/catchand proper fallback - Empty state handled before rendering
SearchPostsis the only Client Component (has"use client") — it handles interactivityPostCardis another Server Component receiving data as props- The page uses ISR with
next: { revalidate: 60 }
Advanced Example
Section titled “Advanced Example”Async Server Component with parallel data fetching and streaming:
// app/dashboard/page.tsx — Advanced Server Componentimport { Suspense } from 'react'import { MetricsCard } from './MetricsCard'import { RecentOrders } from './RecentOrders'import { ActivityFeed } from './ActivityFeed'
// Data fetching functions — run in parallelasync function getMetrics() { const res = await fetch('https://api.example.com/metrics', { next: { revalidate: 30 }, }) return res.json()}
async function getOrders() { const res = await fetch('https://api.example.com/orders/recent', { next: { revalidate: 15 }, }) return res.json()}
async function getActivity() { const res = await fetch('https://api.example.com/activity', { next: { revalidate: 10 }, }) return res.json()}
export default async function DashboardPage() { // Start all fetches in parallel const [metrics, orders, activity] = await Promise.all([ getMetrics(), getOrders(), getActivity(), ])
return ( <div className="dashboard"> <h1>Dashboard</h1>
{/* Server-rendered metrics */} <div className="metrics-grid"> <MetricsCard title="Revenue" value={metrics.revenue} /> <MetricsCard title="Users" value={metrics.users} /> <MetricsCard title="Orders" value={metrics.orders} /> </div>
{/* Each section can also stream independently with Suspense */} <div className="dashboard-grid"> <Suspense fallback={<div>Loading orders...</div>}> <RecentOrders initialOrders={orders} /> </Suspense> <Suspense fallback={<div>Loading activity...</div>}> <ActivityFeed initialActivity={activity} /> </Suspense> </div> </div> )}What’s happening:
Promise.all()fetches all data in parallel, not sequentially- Each data source has its own
revalidatetime for granular cache control - Metrics are server-rendered inline (fastest path)
- Orders and Activity use Suspense boundaries for streaming
- Each section can load independently
Production Example
Section titled “Production Example”Production landing page with Server Components, SEO, and streaming:
// app/page.tsx — Production Server Componentimport { Suspense } from 'react'import type { Metadata } from 'next'import { HeroSection } from './HeroSection'import { FeaturesSection } from './FeaturesSection'import { PricingSection } from './PricingSection'import { TestimonialsSection } from './TestimonialsSection'import { NewsletterSignup } from './NewsletterSignup'
// Static metadata — no data fetching needed (fastest)export const metadata: Metadata = { title: 'MyApp — Modern SaaS Platform', description: 'Build better products with MyApp. Features include analytics, collaboration, and integrations.', openGraph: { title: 'MyApp — Modern SaaS Platform', description: 'Build better products with MyApp.', images: ['/og-image.png'], },}
export default async function LandingPage() { return ( <main> {/* Hero — Server Component, renders first */} <HeroSection />
{/* Features — streams in after hero */} <Suspense fallback={<div className="skeleton" />}> <FeaturesSection /> </Suspense>
{/* Pricing — fetches from CMS with ISR */} <Suspense fallback={<PricingSkeleton />}> <PricingSection /> </Suspense>
{/* Testimonials — slowest data, streams last */} <Suspense fallback={<TestimonialsSkeleton />}> <TestimonialsSection /> </Suspense>
{/* Client Component — newsletter form */} <NewsletterSignup /> </main> )}
function PricingSkeleton() { return <div className="pricing-skeleton">Loading pricing...</div>}
function TestimonialsSkeleton() { return <div className="testimonials-skeleton">Loading testimonials...</div>}Folder Structure
Section titled “Folder Structure”my-app/├── app/│ ├── layout.tsx ← Root layout (Server)│ ├── page.tsx ← Homepage (Server)│ ├── posts/│ │ ├── page.tsx ← Blog listing (Server)│ │ └── [slug]/│ │ └── page.tsx ← Blog post (Server)│ └── dashboard/│ └── page.tsx ← Dashboard (Server with Client islands)│├── components/│ ├── Header.tsx ← Server (no interactivity)│ ├── Footer.tsx ← Server (no interactivity)│ ├── LikeButton.tsx ← Client ('use client')│ ├── SearchInput.tsx ← Client ('use client')│ └── ThemeToggle.tsx ← Client ('use client')│├── lib/│ ├── db.ts ← Server utilities│ └── api.ts ← Shared API helpers└── next.config.js
// Naming convention:// *.tsx without 'use client' = Server Component (default)// *.tsx with 'use client' = Client Component (explicit)🚀 Best Practices
Section titled “🚀 Best Practices”- Default to Server Components — Only add
"use client"when you need hooks, event handlers, or browser APIs - Push Client Components to the leaves — Keep Server Components at the top of your tree; add interactivity at the leaf level
- Use async components for data fetching — Fetch data directly in Server Components instead of creating API routes
- Leverage
Promise.all— Fetch independent data in parallel to avoid waterfall requests - Extract client parts — Move interactive elements (buttons, inputs) into separate Client Component files
- Stream with Suspense — Wrap slow data fetching sections in Suspense boundaries
- Keep heavy deps on server — Markdown parsers, date libraries, and chart utilities belong in Server Components
⚠ Common Mistakes
Section titled “⚠ Common Mistakes”- Adding
"use client"too early — The entire subtree becomes client-rendered, losing server benefits - Forgetting Server Components can’t use hooks —
useState,useEffect,useContextare client-only - Passing complex objects across the boundary — Functions, Date objects, and classes can’t be serialized
- Making all components async — Only Server Components can be async; Client Components cannot
- Not handling errors in async components — Use
error.tsxfor error boundaries - Putting sensitive data in Server Components — They still send rendered output; don’t expose secrets in props
📦 Performance Notes
Section titled “📦 Performance Notes”- Server Components contribute ZERO bytes to the client JavaScript bundle
- Only the RSC payload (serialized component tree) is sent to the client
- Server Components improve Largest Contentful Paint (LCP) through direct HTML generation
- Streaming allows progressive rendering — users see content faster
- Each Server Component can have its own cache TTL and revalidation strategy
🔒 Security Notes
Section titled “🔒 Security Notes”- Server Components access databases and file systems directly — no API endpoint to secure
- Database queries in Server Components are server-side only — not exposed to the client
- Environment variables used in Server Components never reach the browser
- However, rendered output (props, text content) is sent to the client
🌍 SEO Considerations
Section titled “🌍 SEO Considerations”- Server Components produce HTML directly — excellent for SEO (search engines get rendered content)
- No JavaScript required for content rendering — search engine crawlers can index content easily
- Metadata API works naturally with Server Components
- Streaming doesn’t affect SEO — initial HTML is always complete
Interview Questions
Section titled “Interview Questions”- What are React Server Components and how do they differ from Client Components?
- What is the RSC Payload and what does it contain?
- Why can’t Server Components use hooks like useState or useEffect?
- How do you pass data from Server Components to Client Components?
- What happens when a Server Component is a parent of a Client Component?
- Can Server Components be async? Can Client Components be async?
- How does streaming work with Server Components?
-
What directive makes a component a Client Component? a)
"use server"b)"use client"c)"use browser"d) No directive neededAnswer
b) `"use client"` — Adding this directive at the top of the file makes it a Client Component. -
What type is the default component in the App Router? a) Client Component b) Server Component c) Static Component d) Hydrated Component
Answer
b) Server Component — All components in the App Router are Server Components by default. -
Can a Server Component use useState? a) Yes, always b) No, never c) Only if it’s async d) Only in development mode
Answer
b) No — Server Components cannot use hooks; they are rendered on the server where there's no state. -
What is the RSC Payload? a) A JavaScript bundle sent to the client b) A serialized representation of the rendered component tree c) A CSS stylesheet d) An HTML file
Answer
b) The RSC Payload is a serialized format that represents the Server Component tree, enabling React to reconcile updates on the client. -
Can a Server Component import and render a Client Component? a) No, it’s not allowed b) Yes, Server Components can render Client Components c) Only if the Client Component is wrapped in Suspense d) Only with the
"use server"directiveAnswer
b) Yes — Server Components can render Client Components. This is the recommended pattern.
Practice Exercise
Section titled “Practice Exercise”- Create a Server Component page that fetches data from JSONPlaceholder and renders a list of posts
- Add a Client Component for searching/filtering the posts
- Convert a traditional React component to a Server Component — move data fetching inline, remove hooks
- Measure the bundle size difference using browser DevTools (Network tab) between Server and Client versions
Debugging Exercise
Section titled “Debugging Exercise”Find and fix the bugs:
'use client' // Bug 1: This should be a Server Component
import { useState } from 'react' // Bug 2: useState not needed
export default async function UsersPage() { // Bug 3: Client Components can't be async const users = await fetch('https://api.example.com/users') const data = await users.json()
return ( <ul> {data.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> )}Bug 1: 'use client' directive makes this a Client Component unnecessarily.
Fix: Remove 'use client' — it should be a Server Component.
Bug 2: useState is imported but never used.
Fix: Remove the unused import.
Bug 3: Client Components cannot be async.
Fix: After removing 'use client', the async function works perfectly as a Server Component.
Real-world Scenario
Section titled “Real-world Scenario”Problem: Your e-commerce product page needs to show product details (from database), user reviews (from API), and recommended products (from ML service). Each data source has different response times. You need the page to load fast and progressively show content.
Solution:
export default async function ProductPage({ params }: { params: { id: string } }) { return ( <div> {/* Fast: rendered immediately */} <ProductInfo id={params.id} />
{/* Streaming: shows when reviews arrive */} <Suspense fallback={<ReviewsSkeleton />}> <UserReviews productId={params.id} /> </Suspense>
{/* Streaming: shows when ML recommendations arrive */} <Suspense fallback={<RecommendationsSkeleton />}> <RecommendedProducts productId={params.id} /> </Suspense> </div> )}Interview Coding Question
Section titled “Interview Coding Question”Build a Server Component that fetches data from two sources in parallel and renders a combined view:
export default async function DashboardPage() { const [userData, analyticsData] = await Promise.all([ fetchUserData(), fetchAnalytics(), ])
return ( <div> <h1>Welcome back, {userData.name}</h1> <p>You have {analyticsData.activeUsers} active users today</p> </div> )}Mini Project
Section titled “Mini Project”Build a Server-Component Driven Blog
Create a blog with:
- Post listing page — Server Component fetching posts from a database/API
- Individual post pages — Server Component with markdown rendering
- Search component — Client Component filtering posts by title
- Like button — Client Component with interactive state
- Author card — Server Component with related posts
Structure:
app/├── page.tsx ← Server: post listing├── posts/│ └── [slug]/│ └── page.tsx ← Server: individual post├── components/│ ├── PostList.tsx ← Server: renders posts│ ├── PostSearch.tsx ← Client: search input│ └── LikeButton.tsx ← Client: like interaction└── lib/ └── posts.ts ← Server: data fetchingSummary
Section titled “Summary”React Server Components (RSC) are the default component type in the App Router. They run exclusively on the server, reducing client-side JavaScript, enabling direct data access, and improving performance. Server Components can be async, fetch data directly, and access server-side resources. They cannot use hooks, browser APIs, or event handlers. Add "use client" only when you need interactivity — Server Components should be your default choice.
Cheat Sheet
Section titled “Cheat Sheet”// Server Component (default — NO directive needed)export default async function MyComponent() { const data = await fetchData() return <div>{data}</div>}
// Server Component rules:// ✅ Can be async// ✅ Can fetch data directly// ✅ Can access DB, file system// ✅ Zero client-side JS// ❌ Cannot use hooks (useState, useEffect)// ❌ Cannot use browser APIs// ❌ Cannot have event handlers (onClick)
// Client Component (add 'use client' at top)'use client'export default function MyComponent() { const [count, setCount] = useState(0) return <button onClick={() => setCount(c => c + 1)}>{count}</button>}
// Client Component rules:// ✅ Can use hooks// ✅ Can manage state// ✅ Can handle events// ❌ Cannot be async// ❌ Cannot access DB directly// ❌ Sends JS to browser
// Pattern: Server wraps Client// ✅ Server → Client (correct)// ❌ Client → Server (incorrect)Related Topics
Section titled “Related Topics”- Server vs Client Components (Next Topic)
- Data Fetching Patterns (Module 2)
- Streaming & Suspense (Module 4)
- Server Actions (Module 5)