- Published on
React Server Components in Next.js - Complete Guide
- Authors

- Name
- Sanjeev Sharma
- @webcoderspeed1
Introduction
React Server Components (RSC) are the biggest architectural shift in React's history. They run entirely on the server, can fetch data directly without useEffect, and send zero JavaScript to the client.
In Next.js, Server Components are the default — every component is a Server Component unless you add 'use client'.
- Server vs Client Components
- The Mental Model: Two Environments
- Data Fetching in Server Components
- Composing Server and Client Components
- Parallel Data Fetching
- Streaming with Suspense
- Passing Server Data to Client Components
- When to Use Each
- Conclusion
Server vs Client Components
// SERVER COMPONENT (default in Next.js App Router)
// ✅ Can fetch data directly (no useEffect)
// ✅ Can access backend resources (DB, filesystem)
// ✅ Sends ZERO JavaScript to the client
// ❌ Cannot use useState, useEffect, event handlers
// ❌ Cannot use browser APIs
async function ProductList() {
// Direct async/await — no useEffect needed!
const products = await db.product.findAll()
return (
<ul>
{products.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
)
}
// CLIENT COMPONENT — add 'use client' at the top
'use client'
// ✅ Can use useState, useEffect, event handlers
// ✅ Can use browser APIs (window, localStorage)
// ❌ Cannot be async (no direct DB access)
// ❌ All code is bundled and sent to the browser
import { useState } from 'react'
function AddToCartButton({ productId }: { productId: number }) {
const [added, setAdded] = useState(false)
return (
<button onClick={() => setAdded(true)}>
{added ? '✓ Added' : 'Add to Cart'}
</button>
)
}
The Mental Model: Two Environments
SERVER CLIENT (Browser)
───────────────────────────────── ─────────────────
ProductPage (Server) CartButton (Client)
↓ fetches from DB ↓ handles click
↓ renders HTML ↓ updates state
↓ sends to browser ↓ re-renders
Server Components fetch and render. Client Components handle interactivity.
Data Fetching in Server Components
No more useEffect + useState for data fetching:
// ✅ Clean, direct data fetching
async function UsersPage() {
const users = await prisma.user.findMany({
orderBy: { createdAt: 'desc' },
take: 20,
})
return (
<main>
<h1>Users</h1>
{users.map(user => (
<UserCard key={user.id} user={user} />
))}
</main>
)
}
Compare this to the equivalent with the Pages Router:
// Old way — boilerplate
export async function getServerSideProps() {
const users = await prisma.user.findMany()
return { props: { users } }
}
export default function UsersPage({ users }) {
return (/* render */)
}
Server Components eliminate the boilerplate entirely.
Composing Server and Client Components
The key rule: Server Components can import Client Components, but not vice versa (for Server logic).
// Server Component — fetches data
async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id)
const reviews = await getReviews(params.id)
return (
<div>
{/* Static product info — Server Component */}
<ProductInfo product={product} />
{/* Interactive button — Client Component */}
<AddToCartButton productId={product.id} price={product.price} />
{/* Reviews with interactive sort — Client Component */}
<ReviewList reviews={reviews} />
</div>
)
}
'use client' // This component is interactive
import { useState } from 'react'
import { addToCart } from '@/app/actions' // Server Action!
export function AddToCartButton({
productId,
price
}: {
productId: number
price: number
}) {
const [loading, setLoading] = useState(false)
async function handleClick() {
setLoading(true)
await addToCart(productId)
setLoading(false)
}
return (
<button onClick={handleClick} disabled={loading}>
{loading ? 'Adding...' : `Add to Cart — $${price}`}
</button>
)
}
Parallel Data Fetching
Fetch multiple resources simultaneously in Server Components:
async function DashboardPage() {
// ✅ Parallel fetching — all run simultaneously!
const [user, stats, recentOrders] = await Promise.all([
getUser(),
getDashboardStats(),
getRecentOrders(),
])
return (
<div>
<WelcomeBanner user={user} />
<StatsGrid stats={stats} />
<OrdersList orders={recentOrders} />
</div>
)
}
Streaming with Suspense
Stream different parts of your UI as they become ready:
import { Suspense } from 'react'
export default function DashboardPage() {
return (
<main>
{/* Renders immediately — no data needed */}
<DashboardHeader />
{/* Streams in when ready */}
<Suspense fallback={<StatsSkeleton />}>
<StatsSection />
</Suspense>
{/* Streams in independently */}
<Suspense fallback={<OrdersSkeleton />}>
<RecentOrders />
</Suspense>
</main>
)
}
async function StatsSection() {
const stats = await getStats() // Might be slow
return <StatsGrid stats={stats} />
}
async function RecentOrders() {
const orders = await getOrders() // Also might be slow
return <OrdersList orders={orders} />
}
The header renders instantly. Stats and orders stream in independently as each finishes — no waterfall!
Passing Server Data to Client Components
// Server Component — has access to DB
async function UserSettingsPage() {
const user = await getUser() // Server-side only
// Pass data as props to Client Component
return <SettingsForm initialData={user} />
}
'use client'
// Client Component — receives data as props, handles interaction
export function SettingsForm({ initialData }) {
const [name, setName] = useState(initialData.name)
return (
<form>
<input value={name} onChange={e => setName(e.target.value)} />
</form>
)
}
When to Use Each
| Use Case | Component Type |
|---|---|
| Fetching data from DB | Server Component |
| Accessing filesystem | Server Component |
| Using secret keys/env | Server Component |
| Rendering large lists | Server Component |
| useState / useEffect | Client Component |
| Event listeners (onClick) | Client Component |
| Browser APIs (localStorage) | Client Component |
| Animations | Client Component |
Default rule: Start with Server Components. Add 'use client' only when you need interactivity.
Conclusion
React Server Components represent a paradigm shift — moving data fetching back to the server while keeping interactivity in the browser, exactly where each belongs. The result is apps that are faster (less JS shipped), simpler (no useEffect for data fetching), and more secure (secrets stay on the server). Master Server Components and you'll unlock the full power of modern Next.js.