React Server Components (RSC) have matured significantly since their introduction, and in 2026 they are the default way to build performant React applications. If you are still rendering everything on the client, you are leaving performance and SEO gains on the table. This guide walks you through the practical side of React Server Components — what they are, when to use them, and how to build real features with them today.
What Are React Server Components?
React Server Components are components that run exclusively on the server. They never ship JavaScript to the browser, which means they can directly access databases, file systems, and internal APIs without exposing any of that logic to the client. The rendered HTML is streamed to the browser, resulting in smaller bundle sizes and faster page loads.
The key distinction is simple:
- Server Components — Run on the server, zero client-side JS, can use async/await directly
- Client Components — Run in the browser, handle interactivity (state, effects, event handlers)
In Next.js 15+ (the most common RSC framework), every component is a Server Component by default. You opt into client rendering with the "use client" directive.
Setting Up a Project
Let us create a fresh Next.js 15 project with the App Router, which has full RSC support:
npx create-next-app@latest my-rsc-app
cd my-rsc-app
npm run devThe app/ directory uses Server Components by default. Any file like app/page.tsx is automatically a Server Component.
Your First Server Component
Here is a Server Component that fetches blog posts directly from a database using Prisma — no API route needed:
// app/posts/page.tsx (Server Component — no directive needed)
import { prisma } from "@/lib/prisma";
export default async function PostsPage() {
const posts = await prisma.post.findMany({
orderBy: { createdAt: "desc" },
take: 10,
});
return (
<main>
<h1>Latest Posts</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</li>
))}
</ul>
</main>
);
}Notice there is no useEffect, no loading state, no API call. The component is async, fetches data on the server, and sends only HTML to the client. The Prisma import never reaches the browser bundle.
Mixing Server and Client Components
Real apps need interactivity. The pattern is to keep Server Components as the outer layer and nest Client Components inside them for interactive parts.
// app/posts/SearchablePostList.tsx
"use client";
import { useState } from "react";
export default function SearchablePostList({ posts }) {
const [query, setQuery] = useState("");
const filtered = posts.filter((p) =>
p.title.toLowerCase().includes(query.toLowerCase())
);
return (
<div>
<input
type="text"
placeholder="Search posts..."
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<ul>
{filtered.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}Now use it from your Server Component:
// app/posts/page.tsx
import { prisma } from "@/lib/prisma";
import SearchablePostList from "./SearchablePostList";
export default async function PostsPage() {
const posts = await prisma.post.findMany({
orderBy: { createdAt: "desc" },
select: { id: true, title: true, excerpt: true },
});
return (
<main>
<h1>Posts</h1>
<SearchablePostList posts={posts} />
</main>
);
}The data fetching happens on the server. Only the search/filter logic ships to the client. This is the core RSC pattern: fetch on the server, interact on the client.
Server Actions: Handling Mutations
Server Actions let you write server-side mutation functions that can be called directly from Client Components — no API routes required.
// app/posts/actions.ts
"use server";
import { prisma } from "@/lib/prisma";
import { revalidatePath } from "next/cache";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
const content = formData.get("content") as string;
await prisma.post.create({
data: { title, content },
});
revalidatePath("/posts");
}// app/posts/NewPostForm.tsx
"use client";
import { createPost } from "./actions";
import { useActionState } from "react";
export default function NewPostForm() {
return (
<form action={createPost}>
<input name="title" placeholder="Post title" required />
<textarea name="content" placeholder="Write something..." required />
<button type="submit">Publish</button>
</form>
);
}The form submits directly to a server function. No fetch calls, no REST endpoints, no client-side serialization. React handles everything under the hood.
Streaming and Suspense
One of the biggest performance wins with RSC is streaming. You can wrap slow components in <Suspense> to show the rest of the page immediately while data loads:
// app/dashboard/page.tsx
import { Suspense } from "react";
import RevenueChart from "./RevenueChart";
import RecentOrders from "./RecentOrders";
export default function Dashboard() {
return (
<main>
<h1>Dashboard</h1>
<Suspense fallback={<p>Loading chart...</p>}>
<RevenueChart />
</Suspense>
<Suspense fallback={<p>Loading orders...</p>}>
<RecentOrders />
</Suspense>
</main>
);
}Each section streams independently. The user sees the page shell instantly, and each data section pops in as it resolves. No spinners blocking the entire page.
When NOT to Use Server Components
Server Components are not a replacement for everything. Use Client Components when you need:
- useState or useReducer — Any component with local state
- useEffect — Browser-only side effects
- Event handlers — onClick, onChange, onSubmit with client logic
- Browser APIs — localStorage, window, navigator
- Third-party libraries that use hooks or browser APIs internally
The rule of thumb: if it does not need interactivity, keep it as a Server Component. If it does, make it a Client Component and keep it as small as possible.
Performance Comparison
Here is what a typical RSC migration looks like in numbers:
- JS bundle size: 40-60% reduction (server-only code never ships)
- Time to First Byte: Faster with streaming (partial HTML sent immediately)
- Largest Contentful Paint: 20-35% improvement on data-heavy pages
- API routes eliminated: Direct database access from components removes an entire layer
Key Takeaways
- Server Components are the default in Next.js 15+ — embrace them
- Use
"use client"only when you genuinely need browser interactivity - Server Actions replace API routes for mutations
- Suspense boundaries enable streaming for a faster perceived load time
- Keep Client Components small and push data fetching to the server layer
React Server Components are not just a performance optimization — they represent a fundamental shift in how we architect React applications. By moving data fetching and rendering to the server, you write less code, ship less JavaScript, and deliver a faster experience to your users.

Leave a Reply