Rethinking State with React Server Components
React Server Components (RSC) represent the biggest paradigm shift in React's history. Learn how to separate server and client logic for zero-bundle-size components.

“The server is part of your UI tree.”
If you’ve been following React development recently, you’ve likely heard the buzz around React Server Components (RSC). It’s arguably the most significant architectural shift since the introduction of Hooks.
But what problem is it actually solving? And does it mean we’re going back to PHP?
The Problem: The Client-Server Waterfall
In a traditional React SPA (Single Page Application), we often see this pattern:
- Browser downloads dynamic JS bundle.
- App renders a “Skeleton” or “Loading” state.
useEffectruns to fetch User data.- User component renders.
- Inside User,
useEffectruns to fetch Posts. - Posts render.
This “waterfall” of network requests kills performance. We’ve tried to fix it with patterns like getServerSideProps in Next.js, but that forces us to fetch everything at the page level, making it hard to compose components dynamically.
The Solution: Components on the Server
Server Components allow individual components to run on the server, fetch data directly (without an API layer in between), and stream the result to the client.
The magic is that none of the server component’s code ends up in the client bundle.
Import a massive library like moment.js or a markdown parser in a Server Component? Zero KBs added to your main bundle. The user only receives the rendered result.
The New Mental Model
We now split our world into two types of components:
1. Server Components (The Default)
These are components that fetch data, access the database, or render static content. They cannot use useState, useEffect, or event listeners like onClick.
Crucially, there is no “directive” for Server Components. In the RSC world, all components are Server Components by default unless you opt-out.
// app/page.tsx
import { Suspense } from "react";
import db from "./db";
import Loading from "./loading";
// Native async component!
export default async function BlogIndex() {
// Fetch starts immediately
const posts = await db.query("SELECT * FROM posts");
return (
<div>
<h1>My Blog</h1>
<Suspense fallback={<Loading />}>
<PostList posts={posts} />
</Suspense>
</div>
);
}2. Client Components ("use client")
These are your traditional React components. They have state, effects, and interactivity. You opt-in to this mode by adding the "use client" directive at the top of the file.
// app/LikeButton.tsx
"use client";
import { useState } from "react";
export default function LikeButton() {
const [likes, setLikes] = useState(0);
return <button onClick={() => setLikes(likes + 1)}>Like ({likes})</button>;
}3. Server Actions ("use server")
A common misconception is that "use server" marks a Server Component. It does not.
"use server" marks a function as a Server Action—a function that can be called from the client but executes on the server. This replaces the need for manual API routes for mutations.
// app/actions.ts
"use server";
export async function likePost(postId: string) {
// This runs on the server
await db.query("UPDATE posts SET likes = likes + 1 WHERE id = ?", [postId]);
}You can then pass this function directly to a Client Component’s event handler.
Composition: The “Hole” Pattern
The most confusing part for many is how to combine them. You cannot import a Server Component into a Client Component. (Because the Client Component runs in the browser, and the Server Component code doesn’t exist there!).
However, you can pass a Server Component as children to a Client Component.
// app/page.tsx (Server Component)
import ClientWrapper from "./ClientWrapper";
import ServerContent from "./ServerContent";
export default function Page() {
return (
<ClientWrapper>
{/* This works! React passes the rendered result (the "hole") to the wrapper */}
<ServerContent />
</ClientWrapper>
);
}This works because ClientWrapper doesn’t need to know what the children are, just where to put them.
Conclusion
React Server Components aren’t just a performance optimization; they are a new way to think about building interfaces. By embracing the server as part of our component tree, we simplify data fetching and drastically reduce the JavaScript we ship to our users.
The future of React is hybrid.