React Server Components, explained by what they remove
Server Components in Next.js make more sense once you see them as deleting the API layer you used to write by hand.
React Server Components confused me until I stopped asking what they add and started noticing what they remove. The clearest way to understand them is to look at the code you no longer have to write, because that's where the whole idea lands.
The old dance
For years, showing server data in a React app meant a fixed choreography. Build an API endpoint. Fetch from it on the client with a loading state. Handle the error. Wire up caching. Every piece of server data on the page needed this round trip through an API you built and maintained yourself.
// The old way: an endpoint, plus client fetching
function Projects() {
const [projects, setProjects] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("/api/projects")
.then((r) => r.json())
.then((d) => { setProjects(d); setLoading(false); });
}, []);
if (loading) return <Spinner />;
return <List items={projects} />;
}
That's a lot of ceremony to show a list. And somewhere there's an /api/projects route that exists only to feed this component.
What a Server Component does
A Server Component runs on the server, so it can read your data directly and render HTML from it. No endpoint, no client fetch, no loading state.
// The new way: the component IS the data access
async function Projects() {
const projects = await db.project.findMany();
return <List items={projects} />;
}
The async component queries the database and returns markup. The browser receives HTML that's already populated. The API route is gone because the component does its own data access on the server. That /api/projects endpoint you were maintaining, along with its client fetch and its spinner, all deleted.
The line that trips everyone up
The catch is that Server Components run only on the server, so they can't use anything browser-only: no useState, no useEffect, no click handlers. The moment you need interactivity, you mark that component with a directive.
"use client";
function LikeButton() {
const [liked, setLiked] = useState(false);
return <button onClick={() => setLiked(!liked)}>♥</button>;
}
"use client" opts a component back into the browser, with all the interactivity you're used to. The mental model that finally clicked for me: Server Components are the default, and you reach for "use client" only at the leaves of the tree that actually need interaction. Most of your app is static structure and data, which stays on the server. The interactive bits are small islands you explicitly mark.
Composing the two
The pattern that follows is a server component doing the data work, passing results down to small client components for interaction.
async function ProjectPage({ id }: { id: string }) {
const project = await db.project.findUnique({ where: { id } });
return (
<article>
<h1>{project.title}</h1>
<LikeButton projectId={project.id} /> {/* client island */}
</article>
);
}
The heavy lifting (the query, the layout) happens on the server and arrives as ready HTML. Only the LikeButton ships JavaScript. The page is fast because you're not shipping a data-fetching library and a spinner and the fetch logic to every visitor, only the interactive scrap that needs it.
Why this matters for performance
Less client JavaScript is the headline. A traditional React app ships the component code plus the fetching logic plus the data-shaping code to the browser, then does the work there. Server Components do that work once on the server and send the result. The user's device downloads and runs less, which matters most on the mid-range phones that make up a lot of real traffic. My whole approach to keeping this site fast is built on that principle, doing work where it's cheapest and shipping only what has to run in the browser.
The honest friction
It's not free. The server/client boundary is a real thing you have to hold in your head, and the error you'll hit most is using a hook in a server component, which fails with a message that isn't always obvious the first time. You also can't pass functions as props across the boundary, since functions don't serialize. Once the "server by default, client at the leaves" model settles in, these stop being surprises.
But the payoff is genuine. Deleting the hand-written API layer for data that only your own frontend consumes removes a whole category of code, and code you don't write is code that can't break. That's the part that won me over: not what Server Components add, but the maintenance they quietly take away. If you're choosing a frontend stack, that deletion is a real point in React's favour.