How Server Components (RSC) Work in Next.js
React Server Components (RSC) are components that execute exclusively on the server. Their code never ends up in the JavaScript bundle that the browser downloads. In the Next.js App Router (starting from version 13) all components are server components by default.
Why Server Components Exist
Before RSC all React code was sent to the browser. Even if a component simply displayed data from a database, its code, dependencies and logic were included in the client bundle.
Server Components solve three problems:
-
Bundle size. Server component code and their dependencies are not sent to the client. If you use a heavy data processing library, it stays on the server.
-
Direct data access. Server components can directly access the database, file system, environment variables. No intermediate API layer needed.
-
Security. Secrets (API keys, database connection strings) never reach the client.
Server vs Client Components
// Server component (default)
// Runs on the server, code is not included in the bundle
import { db } from '@/lib/db'
export default async function ProblemsCount() {
const count = await db.problem.count()
return <p>Problems on Hack Frontend: {count}</p>
}
// Client component
// Marked with the 'use client' directive
'use client'
import { useState } from 'react'
export default function LikeButton() {
const [liked, setLiked] = useState(false)
return (
<button onClick={() => setLiked(!liked)}>
{liked ? 'Unlike' : 'Like'}
</button>
)
}
When to Use Which
| Task | Server | Client |
|---|---|---|
| Fetching data from DB/API | ✓ | |
| Accessing env variables, files | ✓ | |
| Heavy dependencies (markdown parser, etc.) | ✓ | |
| useState, useEffect, useRef | ✓ | |
| onClick, onChange, onSubmit | ✓ | |
| Browser API (localStorage, geolocation) | ✓ | |
| React Context (useContext) | ✓ |
How It Works Under the Hood
Rendering on the server
Next.js executes server components and creates a special format (RSC Payload) that describes the rendering result.
Streaming to the client
The RSC Payload is sent to the browser along with HTML. Client components are hydrated, server components are already rendered.
UI updates
On navigation Next.js requests the RSC Payload for the new route. Server components re-render on the server, client components keep their state.
Usage Patterns
Server Component Wraps Client Component
The most common pattern: a server component loads data and passes it to a client component via props:
// app/problems/page.tsx (server)
import { db } from '@/lib/db'
import { ProblemList } from './problem-list'
export default async function ProblemsPage() {
const problems = await db.problem.findMany({
orderBy: { difficulty: 'asc' }
})
return <ProblemList problems={problems} />
}
// app/problems/problem-list.tsx (client)
'use client'
import { useState } from 'react'
export function ProblemList({ problems }) {
const [filter, setFilter] = useState('all')
const filtered = filter === 'all'
? problems
: problems.filter(p => p.difficulty === Number(filter))
return (
<div>
<select onChange={e => setFilter(e.target.value)}>
<option value="all">All</option>
<option value="1">Easy</option>
<option value="2">Medium</option>
<option value="3">Hard</option>
</select>
<ul>
{filtered.map(p => (
<li key={p.id}>{p.name}</li>
))}
</ul>
</div>
)
}
Server Component as Children
A client component can receive a server component via children. This lets you create interactive wrappers without losing the benefits of server rendering:
// Client wrapper component
'use client'
import { useState } from 'react'
export function Accordion({ title, children }) {
const [open, setOpen] = useState(false)
return (
<div>
<button onClick={() => setOpen(!open)}>{title}</button>
{open && children}
</div>
)
}
// Server component uses the client wrapper
import { Accordion } from './accordion'
import { db } from '@/lib/db'
export default async function FAQ() {
const items = await db.faq.findMany()
return (
<div>
{items.map(item => (
<Accordion key={item.id} title={item.question}>
<p>{item.answer}</p>
</Accordion>
))}
</div>
)
}
Important:
You cannot import a server component inside a client component. If you add 'use client' to a file, all its imports become client-side too. Pass server components only via props or children.
Server Components Limitations
- Cannot use hooks (useState, useEffect, useContext, etc.)
- Cannot add event handlers (onClick, onChange)
- Cannot use browser APIs
- Cannot be imported directly in client components
Interview tip:
A common interview mistake: confusing Server Components with SSR. SSR renders the entire React tree on the server once and sends HTML. Server Components are a different model: part of the tree lives on the server permanently and never sends its code to the client. On navigation, server components re-render on the server.
Useful Resources
- nextjs.org/docs — server components documentation
- react.dev — RSC in React documentation
- Composition Patterns — patterns for combining server and client components