React Server Components: What They Are and When to Use Them
Server Components fundamentally change how React applications work. They're not just a performance optimization—they're a different mental model for building UIs.
Let me explain what's actually happening and how to think about them.
The Core Idea
Traditional React components run in the browser. Your JavaScript bundle ships to the client, React executes it, and components render.
Server Components flip this: they run on the server, render to HTML (plus a special format React understands), and the result gets sent to the client. The component code never reaches the browser.
// This component runs on the server
// Its code is never sent to the browser
async function RecentPosts() {
const posts = await db.post.findMany({
take: 5,
orderBy: { createdAt: 'desc' },
})
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}Notice the async. Server Components can be async functions—they can await data directly. No useEffect, no loading states for initial render. The component waits for its data, renders, and sends the result.
Server vs. Client Components
In Next.js App Router, every component is a Server Component by default. To make a Client Component, you add 'use client' at the top:
'use client'
import { useState } from 'react'
export function Counter() {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
)
}Here's what each type can do:
Server Components can:
Access backend resources directly (database, file system)
Use
async/awaitfor data fetchingKeep sensitive logic and API keys on the server
Reduce client bundle size (their code isn't shipped)
Server Components cannot:
Use hooks like
useState,useEffect,useContextAccess browser APIs (
window,localStorage, etc.)Use event handlers (
onClick,onChange, etc.)Use class components
Client Components can:
Use all React hooks
Handle user interactions
Access browser APIs
Maintain client-side state
The Mental Model
Think of your component tree as having a "client boundary." Once you mark a component with 'use client', that component AND all its children become Client Components—even if they don't have the directive.
// Server Component (default)
export default function Page() {
return (
<div>
<Header /> {/* Server Component */}
<ArticleContent /> {/* Server Component */}
<CommentSection /> {/* Client Component */}
</div>
)
}
// Header.tsx - no directive needed, it's a Server Component
export function Header() {
return <header>...</header>
}
// CommentSection.tsx - needs interactivity
'use client'
export function CommentSection() {
const [comments, setComments] = useState([])
// This component and its children are Client Components
return <div>...</div>
}Practical Patterns
Pattern 1: Push Client Components Down
Make the smallest possible components Client Components:
// Bad: Entire page is client-side
'use client'
export default function ProductPage() {
const [quantity, setQuantity] = useState(1)
return (
<div>
<ProductInfo /> {/* Could be server-rendered! */}
<ProductReviews /> {/* Could be server-rendered! */}
<QuantitySelector {/* Only this needs client */}
quantity={quantity}
onChange={setQuantity}
/>
</div>
)
}
// Good: Only interactive parts are client-side
export default async function ProductPage({ params }) {
const product = await getProduct(params.id)
return (
<div>
<ProductInfo product={product} />
<ProductReviews productId={product.id} />
<AddToCartButton product={product} /> {/* Client Component */}
</div>
)
}Pattern 2: Pass Server Data to Client Components
Server Components can fetch data and pass it as props:
// Server Component
export default async function Dashboard() {
const initialData = await fetchDashboardData()
return (
<DashboardClient initialData={initialData} />
)
}
// Client Component
'use client'
export function DashboardClient({ initialData }) {
const [data, setData] = useState(initialData)
// Now you have server-fetched data as initial state
// and can update it client-side
}Pattern 3: Composition for Context Providers
You can't use context in Server Components, but you can wrap them:
// app/providers.tsx
'use client'
import { ThemeProvider } from 'next-themes'
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider attribute="class">
{children}
</ThemeProvider>
)
}
// app/layout.tsx (Server Component)
import { Providers } from './providers'
export default function RootLayout({ children }) {
return (
<html>
<body>
<Providers>
{children} {/* Server Components work inside! */}
</Providers>
</body>
</html>
)
}The children passed through a Client Component can still be Server Components.
Pattern 4: Streaming with Suspense
Server Components work beautifully with Suspense for streaming:
import { Suspense } from 'react'
export default function Page() {
return (
<div>
<Header /> {/* Renders immediately */}
<Suspense fallback={<PostsSkeleton />}>
<RecentPosts /> {/* Streams in when ready */}
</Suspense>
<Suspense fallback={<CommentsSkeleton />}>
<Comments /> {/* Streams in independently */}
</Suspense>
</div>
)
}The page shell shows immediately, then each Suspense boundary fills in as its data arrives. Users see content faster, and slow data sources don't block the entire page.
When to Use Which
Use Server Components when:
Fetching data
Accessing backend services
Rendering static or mostly-static content
The component doesn't need interactivity
You want to keep code/dependencies off the client
Use Client Components when:
You need
useState,useEffect, or other hooksYou need event handlers
You're using browser APIs
You need React context (though providers can wrap Server Components)
You're using client-side libraries (many UI libraries require client)
Common Mistakes
Mistake 1: Adding 'use client' to everything
Don't do this out of habit. Every component you make a Client Component ships to the browser and adds to your bundle.
Mistake 2: Trying to use hooks in Server Components
This just won't work. If you need state, that component needs to be a Client Component.
Mistake 3: Importing Server Components into Client Components
This doesn't work directly. Instead, pass them as children or other React node props.
The Bigger Picture
Server Components aren't just about performance—they simplify your mental model. Data fetching happens where the data lives (the server). Interactivity happens where the user is (the browser). Each component runs in the environment that makes sense for what it does.
It takes some adjustment, but once it clicks, you'll find yourself writing simpler code that performs better by default.