React Server Components (RSC) are the biggest shift in React architecture since hooks. Here's what you actually need to know.
What Are Server Components?
Server Components render on the server and send HTML (and a serialized component tree) to the client. They have no JavaScript bundle size impact, can access databases directly, and can't use state or effects.
Client Components render on the client (browser). They can use state, effects, event handlers, and browser APIs.
In Next.js 13+, all components are Server Components by default. Add 'use client' to opt into a Client Component.
The Mental Model
Server Components:
- Run on server only
- Zero bundle size (JS not sent to browser)
- Can access databases, file system, secrets
- Can't use useState, useEffect, event handlers
- Can't use browser APIs
Client Components:
- Run in browser (and optionally on server via SSR)
- Added to JS bundle
- Can use all React hooks
- Can use browser APIs (window, document, etc.)
- Receive data from server as props
Data Fetching Patterns
// Server Component -- fetch directly, no useEffect needed
// app/products/page.tsx (Server Component by default)
async function ProductsPage() {
// This runs on the server -- DB access is fine here
const products = await db.query('SELECT * FROM products WHERE active = true')
return (
<div>
<h1>Products</h1>
{products.map(p => (
<ProductCard key={p.id} product={p} />
))}
</div>
)
}
// ProductCard can be a Server Component too if it doesn't need interactivity
function ProductCard({ product }: { product: Product }) {
return (
<div>
<h2>{product.name}</h2>
<p>{product.price}</p>
<AddToCartButton productId={product.id} /> {/* Client Component */}
</div>
)
}
// Client Component -- interactive
'use client'
import { useState } from 'react'
function AddToCartButton({ productId }: { productId: string }) {
const [added, setAdded] = useState(false)
return (
<button onClick={() => {
addToCart(productId)
setAdded(true)
}}>
{added ? 'Added!' : 'Add to Cart'}
</button>
)
}
Server Components Can't Import Client Components
Actually they can -- but only as leaves. The boundary flows one way:
Server Component
-> can render -> Server Component ✓
-> can render -> Client Component ✓
Client Component
-> can render -> Client Component ✓
-> can render -> Server Component ✗ (won't work)
-> EXCEPT as children passed from Server ✓
// This works! Server component passed as children to Client
// ServerParent.tsx (Server Component)
import { Modal } from './Modal' // Client Component
export default function Page() {
const data = await fetchData()
return (
<Modal>
<ServerContent data={data} /> {/* Stays a Server Component */}
</Modal>
)
}
Streaming with Suspense
// app/dashboard/page.tsx
import { Suspense } from 'react'
import { Skeleton } from '@/components/ui/skeleton'
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
{/* This streams in as soon as RevenueChart is ready */}
<Suspense fallback={<Skeleton className="h-48" />}>
<RevenueChart /> {/* Slow data source */}
</Suspense>
{/* This streams independently */}
<Suspense fallback={<Skeleton className="h-32" />}>
<RecentActivity /> {/* Different data source */}
</Suspense>
</div>
)
}
// Each of these fetches independently and streams when ready
async function RevenueChart() {
const data = await fetchRevenue() // Can take 500ms
return <Chart data={data} />
}
async function RecentActivity() {
const events = await fetchEvents() // Can take 200ms
return <ActivityList events={events} />
}
Common Pitfalls
1. Context in Server Components
// Context doesn't work in Server Components
// Bad: using createContext at the server level
// Good: put Context in a Client Component wrapper
'use client'
import { createContext } from 'react'
export const ThemeContext = createContext('dark')
2. Event handlers in Server Components
// Will throw an error
// Server Component
function Page() {
return <button onClick={() => alert('hi')}>Click</button>
// ^ Error: Event handlers cannot be passed to Client Component props
}
Mark the component 'use client' or extract the button into a Client Component.
3. Date/time in Server Components
// Different output on server vs client = hydration mismatch
function ServerComponent() {
return <p>{new Date().toLocaleString()}</p> // Mismatch!
}
// Fix: use suppressHydrationWarning or fetch at request time
Decision Tree
Does this component use:
- useState / useReducer? -> Client
- useEffect / useLayoutEffect? -> Client
- Browser APIs (window, document)? -> Client
- Event handlers (onClick, onChange)? -> Client
- Custom hooks that use any of the above? -> Client
Otherwise, default to Server Component.
Server Components shine for: data fetching, static content, layout, large dependencies (syntax highlighters, markdown parsers) -- keep them off the client bundle.