React Server Components (RSC) are one of the biggest shifts in the React programming model in years. Instead of shipping every component to the browser, you can render pieces of your UI on the server and stream the result to the client. The payoff is smaller bundles, faster loads, and direct access to your data layer.
The core mental model
Every component is a Server Component by default in the App Router. It only
becomes a Client Component when you opt in with the "use client" directive.
// Server Component — runs on the server, never ships to the browser
import { db } from "@/lib/db"
import { LikeButton } from "./like-button"
export default async function Page() {
const posts = await db.post.findMany() // direct DB access, no API route
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
{post.title}
<LikeButton postId={post.id} />
</li>
))}
</ul>
)
}The LikeButton needs interactivity, so it lives in its own file:
"use client"
import { useState } from "react"
export function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false)
return (
<button onClick={() => setLiked((v) => !v)}>
{liked ? "♥ Liked" : "♡ Like"}
</button>
)
}What runs where?
| Capability | Server Component | Client Component |
|---|---|---|
async/await |
✅ | ⚠️ (via hooks) |
| Access to secrets | ✅ | ❌ |
useState/useEffect |
❌ | ✅ |
| Adds to JS bundle | ❌ | ✅ |
Common pitfalls
- Importing server-only code into a client component. Use the
server-onlypackage to fail fast at build time. - Passing non-serializable props across the boundary. Functions and class instances can't cross from server to client.
- Marking everything
"use client". You lose the benefits — push the boundary as far down the tree as you can.
Rule of thumb: keep components on the server unless they need state, effects, or browser-only APIs.
With this model in place, you get the best of both worlds: a fast, data-rich server render and precise islands of interactivity where they matter.