React Server Components are not SSR. They are a fundamentally new way to build React apps where components run on the server and send zero JavaScript to the client.
The Key Insight
Server Components run on the server and never re-render on the client. They send their rendered output (not JavaScript) to the browser.
Server Component: Runs on server → Sends HTML/RSC payload → Zero JS on client
Client Component: Runs on both → Sends JavaScript → Hydrates on client
Server Components (Default in Next.js App Router)
// app/posts/page.tsx (Server Component by default)
import { db } from '@/lib/db';
export default async function PostsPage() {
// Direct database access! No API needed.
const posts = await db.posts.findMany({
include: { author: true },
orderBy: { createdAt: 'desc' },
});
return (
<div>
<h1>Posts</h1>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>By {post.author.name}</p>
<p>{post.content}</p>
</article>
))}
</div>
);
}
This component:
- Queries the database directly (no API route!)
- Sends zero JavaScript to the browser
- Cannot use useState, useEffect, or event handlers
Client Components
// components/LikeButton.tsx
'use client'; // This makes it a Client Component
import { useState } from 'react';
export function LikeButton({ postId, initialLikes }) {
const [likes, setLikes] = useState(initialLikes);
const [liked, setLiked] = useState(false);
const handleLike = async () => {
setLiked(!liked);
setLikes(l => liked ? l - 1 : l + 1);
await fetch(`/api/posts/${postId}/like`, { method: 'POST' });
};
return (
<button onClick={handleLike}>
{liked ? 'Unlike' : 'Like'} ({likes})
</button>
);
}
Mixing Server and Client
// app/posts/[id]/page.tsx (Server Component)
import { LikeButton } from '@/components/LikeButton';
import { CommentSection } from '@/components/CommentSection';
export default async function PostPage({ params }) {
const post = await db.posts.findUnique({
where: { id: params.id },
include: { author: true },
});
return (
<article>
{/* Server-rendered, zero JS */}
<h1>{post.title}</h1>
<p>By {post.author.name}</p>
<div dangerouslySetInnerHTML={{ __html: post.htmlContent }} />
{/* Client Components — only THESE send JavaScript */}
<LikeButton postId={post.id} initialLikes={post.likes} />
<CommentSection postId={post.id} />
</article>
);
}
Server Actions
// app/posts/new/page.tsx
export default function NewPostPage() {
async function createPost(formData: FormData) {
'use server'; // This runs on the server!
const title = formData.get('title') as string;
const content = formData.get('content') as string;
await db.posts.create({
data: { title, content, authorId: getCurrentUserId() },
});
redirect('/posts');
}
return (
<form action={createPost}>
<input name="title" placeholder="Title" required />
<textarea name="content" placeholder="Content" required />
<button type="submit">Publish</button>
</form>
);
}
When to Use Which
| Need | Use |
|---|---|
| Fetch data | Server Component |
| Read files, env vars | Server Component |
| Use database directly | Server Component |
| Use useState/useEffect | Client Component |
| Event handlers (onClick) | Client Component |
| Browser APIs | Client Component |
| Forms with JS validation | Client Component |
| Static content | Server Component |
Bundle Size Impact
Traditional React App: 250KB JavaScript
With Server Components: ~50KB JavaScript (only interactive parts)
Building data-rich applications? Check out my Apify actors — extract data from any website for your server components. For custom solutions, email spinov001@gmail.com.
Top comments (0)