React Server Components (RSC) are the biggest architectural shift in React since hooks. Components are server-rendered by default, and client-side interactivity is explicitly opted into. This changes how you think about building React applications.
The Mental Model
In the RSC model, components are server components by default. They:
- Run on the server at request time (or build time)
- Can directly access databases, file systems, and server-only APIs
- Send rendered HTML to the client, not JavaScript
- Cannot use state, effects, or browser APIs
// This is a Server Component (default)
async function PostList() {
const posts = await db.posts.findMany({
orderBy: { createdAt: "desc" },
take: 10,
});
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
<a href={`/posts/${post.id}`}>{post.title}</a>
</li>
))}
</ul>
);
}
No API route. No useEffect + useState dance. The component fetches data directly.
Client Components
When you need interactivity, mark a component with "use client":
"use client";
import { useState } from "react";
function LikeButton({ postId, initialCount }: Props) {
const [count, setCount] = useState(initialCount);
async function handleClick() {
setCount((c) => c + 1);
await fetch(`/api/posts/${postId}/like`, { method: "POST" });
}
return <button onClick={handleClick}>Like ({count})</button>;
}
Client components are regular React components. They ship JavaScript to the browser and can use all the hooks and browser APIs you’re used to.
The Boundary
Server components can import and render client components. Client components cannot import server components — but they can receive server components as children or other props.
// Server Component
import { LikeButton } from "./LikeButton"; // client component
async function Post({ id }: { id: string }) {
const post = await getPost(id);
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
<LikeButton postId={id} initialCount={post.likes} />
</article>
);
}
The server component fetches data and renders the static parts. The client component handles the interactive button.
What Changes
Data fetching simplifies. No more getServerSideProps, useEffect + loading states for initial data, or client-side caching libraries for read-heavy pages.
Bundle size decreases. Server components and their dependencies (markdown parsers, date libraries, database clients) never ship to the browser.
Composition patterns shift. The boundary between server and client code becomes an explicit architectural decision rather than an afterthought.
What Doesn’t Change
- Client components still work like they always have
- State management for interactive UI is still needed
- You still need API routes for mutations in many cases
- The React component model (props, composition, JSX) is the same
RSC isn’t a rewrite. It’s a new default that pushes work to the server where it belongs, while keeping the client interactive where it needs to be.