React Server Components (RSC) represent a paradigm shift in how we build React applications. They run only on the server, reducing bundle size and improving performance.
What Are Server Components?
Server Components are a new type of React component that render exclusively on the server. They can directly access backend resources without API layers.
// Server Component (runs on server)
// Note the .server.js suffix
async function BlogPost({ id }) {
const post = await db.post.findUnique({ where: { id } });
const comments = await db.comment.findMany({ where: { postId: id } });
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
<CommentsList comments={comments} />
</article>
);
}
// Client Component (runs in browser)
'use client';
import { useState } from 'react';
function LikeButton() {
const [likes, setLikes] = useState(0);
return <button onClick={() => setLikes(l => l + 1)}>{likes}</button>;
}Key Benefits
Zero Bundle Size
Server Components don't add to your JavaScript bundle.
// This entire component adds 0 bytes to your client bundle
async function ProductsList() {
const products = await fetch('https://api.example.com/products').then(r => r.json());
return (
<div>
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}Direct Backend Access
No need for API endpoints.
// Before: Client Component + API
// Client component
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(setUser);
}, [userId]);
return user ? <Profile user={user} /> : <Loading />;
}
// API route
export async function GET(request, { params }) {
const user = await db.user.findUnique({ where: { id: params.userId } });
return Response.json(user);
}
// After: Server Component
async function UserProfile({ userId }) {
const user = await db.user.findUnique({ where: { id: userId } });
return <Profile user={user} />;
}Automatic Code Splitting
Load only what you need.
import { Suspense } from 'react';
function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<StatsSkeleton />}>
<StatsSection />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<ChartsSection />
</Suspense>
<Suspense fallback={<RecentSkeleton />}>
<RecentActivity />
</Suspense>
</div>
);
}Server vs Client Components
Server Components
- Run only on server
- Can access databases, filesystem, internal APIs
- Have no interactivity (no useState, useEffect)
- Reduce client bundle size
// ✅ Server Component
async function ProductsPage() {
const products = await db.product.findMany();
return (
<div>
<h1>Products</h1>
{products.map(product => (
<div key={product.id}>
<h2>{product.name}</h2>
<p>{product.description}</p>
</div>
))}
</div>
);
}Client Components
- Run in browser
- Have full React features (hooks, event handlers)
- Can use browser APIs
- Add to bundle size
// ✅ Client Component
'use client';
import { useState } from 'react';
function AddToCart({ productId }) {
const [adding, setAdding] = useState(false);
const handleAdd = async () => {
setAdding(true);
await fetch('/api/cart/add', {
method: 'POST',
body: JSON.stringify({ productId }),
});
setAdding(false);
};
return (
<button onClick={handleAdd} disabled={adding}>
{adding ? 'Adding...' : 'Add to Cart'}
</button>
);
}Composition Patterns
Server Components Containing Client Components
// Server Component
async function ProductPage({ id }) {
const product = await db.product.findUnique({ where: { id } });
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<AddToCart productId={product.id} />
</div>
);
}
// Client Component
'use client';
function AddToCart({ productId }) {
// Interactive logic here
}Client Components Containing Server Components
Not directly possible. Server Components must be passed as children.
// ❌ Won't work
'use client';
function Layout({ productId }) {
return <ProductDetails id={productId} />;
}
// ✅ Works - pass as children
'use client';
function Layout({ children }) {
return <div className="layout">{children}</div>;
}
// Usage
<Layout>
<ProductDetails id={productId} />
</Layout>Data Fetching Patterns
Sequential Data Fetching
async function ProductPage({ id }) {
// Wait for product
const product = await db.product.findUnique({ where: { id } });
// Then wait for reviews (uses product data)
const reviews = await db.review.findMany({
where: { productId: product.id }
});
return <ProductDetails product={product} reviews={reviews} />;
}Parallel Data Fetching
import { Suspense } from 'react';
async function ProductPage({ id }) {
return (
<div>
<h1>Product Details</h1>
<Suspense fallback={<ReviewsSkeleton />}>
<Reviews productId={id} />
</Suspense>
<Suspense fallback={<RelatedSkeleton />}>
<RelatedProducts productId={id} />
</Suspense>
</div>
);
}
async function Reviews({ productId }) {
const reviews = await db.review.findMany({
where: { productId }
});
return <ReviewsList reviews={reviews} />;
}Prefetching
'use client';
import { useRouter } from 'next/navigation';
function ProductLink({ productId, children }) {
const router = useRouter();
const handleMouseEnter = () => {
router.prefetch(`/products/${productId}`);
};
return (
<Link
href={`/products/${productId}`}
onMouseEnter={handleMouseEnter}
>
{children}
</Link>
);
}Streaming with Suspense
Progressive Rendering
import { Suspense } from 'react';
function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<StatsLoading />}>
<Stats />
</Suspense>
<Suspense fallback={<ChartsLoading />}>
<Charts />
</Suspense>
<Suspense fallback={<ActivityLoading />}>
<RecentActivity />
</Suspense>
</div>
);
}
async function Stats() {
const stats = await fetch('/api/stats').then(r => r.json());
return <StatsDisplay stats={stats} />;
}Loading States
// loading.js (Next.js)
export default function Loading() {
return (
<div className="loading">
<Spinner />
<p>Loading...</p>
</div>
);
}
// error.js
'use client';
export default function Error({ error, reset }) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
);
}Server Actions
Defining Actions
// actions.ts
'use server';
export async function addTodo(formData) {
const title = formData.get('title');
await db.todo.create({ data: { title } });
revalidatePath('/todos');
}
export async function deleteTodo(id) {
await db.todo.delete({ where: { id } });
revalidatePath('/todos');
}Using Actions
import { addTodo } from './actions';
function TodoForm() {
return (
<form action={addTodo}>
<input name="title" type="text" required />
<button type="submit">Add Todo</button>
</form>
);
}With Loading States
'use client';
import { useFormStatus } from 'react-dom';
import { addTodo } from './actions';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Adding...' : 'Add Todo'}
</button>
);
}
function TodoForm() {
return (
<form action={addTodo}>
<input name="title" type="text" required />
<SubmitButton />
</form>
);
}Migration Strategy
Gradual Migration
// Step 1: Keep existing Client Components
'use client';
function OldProductList() {
const [products, setProducts] = useState([]);
useEffect(() => {
fetch('/api/products').then(r => r.json()).then(setProducts);
}, []);
return <div>{/* ... */}</div>;
}
// Step 2: Create new Server Component
async function NewProductList() {
const products = await db.product.findMany();
return <div>{/* ... */}</div>;
}
// Step 3: Use where appropriate
function ProductsPage() {
return (
<>
<OldProductList /> {/* Keep for interactivity */}
<NewProductList /> {/* Use for static content */}
</>
);
}Best Practices
DOs
- Use Server Components by default
- Use Client Components only when needed
- Keep Client Components small and focused
- Server Component → Client Component (not vice versa)
- Use Suspense for loading states
- Optimize database queries
DON'Ts
- Put interactive logic in Server Components
- Use browser APIs in Server Components
- Overuse Client Components
- Forget to mark Client Components with 'use client'
- Ignore loading states
- Necessarily convert everything to Server Components
Performance Tips
Reduce Client Bundle
// ❌ Large client bundle
'use client';
import { Chart, Calendar, Map, Editor } from 'big-library';
function Dashboard() {
return (
<div>
<Chart />
<Calendar />
<Map />
<Editor />
</div>
);
}
// ✅ Server Components (0 bundle cost)
import { Chart } from 'chart-library/server';
import { Calendar } from 'calendar-library/server';
import { Map } from 'map-library/server';
import { Editor } from 'editor-library/server';
function Dashboard() {
return (
<div>
<Chart />
<Calendar />
<Map />
<Editor />
</div>
);
}Optimize Data Fetching
// ❌ Sequential requests
async function Page() {
const user = await fetchUser();
const posts = await fetchPosts(); // Waits for user
const comments = await fetchComments(); // Waits for posts
return <Page user={user} posts={posts} comments={comments} />;
}
// ✅ Parallel requests
async function Page() {
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments(),
]);
return <Page user={user} posts={posts} comments={comments} />;
}Conclusion
React Server Components enable better performance and developer experience by moving more logic to the server. Use them by default, and reach for Client Components when you need interactivity. The future of React is server-first!