Server Components vs Client Components in Next.js: The Complete Guide
Master the mental model for React Server Components in Next.js. Covers when to use each, data fetching patterns, composition strategies, and common...
The Mental Model
In Next.js App Router, all components are Server Components by default. They render on the server, send HTML to the client, and never ship JavaScript to the browser. Client Components opt into interactivity with the "use client" directive.
A well-structured configuration file is the foundation of reproducible infrastructure.
Think of it this way:
- Server Components: HTML templates that can access databases, file systems, and APIs directly
- Client Components: Interactive widgets that run in the browser
When to Use Server Components
Use Server Components (the default) when you:
- Fetch data from a database or API
- Access backend resources (file system, environment variables)
- Keep sensitive logic on the server (API keys, business rules)
- Render large dependencies that should not ship to the client
- Display static or rarely changing content
// app/blog/page.tsx — Server Component (default, no directive)
import { db } from "@/lib/database";
export default async function BlogPage() {
// This runs on the server — never exposed to the client
const posts = await db.query("SELECT * FROM posts WHERE published = true ORDER BY created_at DESC");
return (
<div>
<h1>Blog</h1>
{posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
<time>{new Date(post.created_at).toLocaleDateString()}</time>
</article>
))}
</div>
);
}
Zero JavaScript shipped to the client. The database query runs on the server.
Get more insights on Tutorials
Join 2,000+ engineers who get our weekly deep-dives. No spam, unsubscribe anytime.
When to Use Client Components
Use Client Components when you need:
- Event handlers (onClick, onChange, onSubmit)
- State (useState, useReducer)
- Effects (useEffect)
- Browser APIs (window, localStorage, navigator)
- Custom hooks that use state or effects
- Third-party libraries that use state/effects
// components/SearchBar.tsx — Client Component
"use client";
import { useState, useCallback } from "react";
import { useRouter, useSearchParams } from "next/navigation";
export function SearchBar() {
const router = useRouter();
const searchParams = useSearchParams();
const [query, setQuery] = useState(searchParams.get("q") || "");
const handleSearch = useCallback(() => {
const params = new URLSearchParams();
if (query) params.set("q", query);
router.push("/search?" + params.toString());
}, [query, router]);
return (
<div>
<input
type="text"
value={query}
=> setQuery(e.target.value)}
=> e.key === "Enter" && handleSearch()}
placeholder="Search..."
/>
<button
</div>
);
}
The Composition Pattern
The most powerful pattern is composing Server Components inside Client Components using the children prop:
// components/Tabs.tsx — Client Component (interactive wrapper)
"use client";
import { useState, ReactNode } from "react";
interface TabsProps {
tabs: { label content: ReactNode }[];
}
export function Tabs({ tabs }: TabsProps) {
const [activeTab, setActiveTab] = useState(0);
return (
<div>
<div role="tablist">
{tabs.map((tab, i) => (
<button
key={i}
role="tab"
=> setActiveTab(i)}
aria-selected={i === activeTab}
>
{tab.label}
</button>
))}
</div>
<div role="tabpanel">{tabs[activeTab].content}</div>
</div>
);
}
// app/dashboard/page.tsx — Server Component passing server-rendered content to client tabs
import { Tabs } from "@/components/Tabs";
import { db } from "@/lib/database";
export default async function DashboardPage() {
const stats = await db.query("SELECT * FROM dashboard_stats");
const activity = await db.query("SELECT * FROM recent_activity LIMIT 20");
return (
<Tabs
tabs={[
{
label: "Overview",
content: (
<div>
<h2>Statistics</h2>
<p>Total users: {stats.totalUsers}</p>
<p>Revenue: {stats.revenue}</p>
</div>
),
},
{
label: "Activity",
content: (
<ul>
{activity.map((item) => (
<li key={item.id}>{item.description}</li>
))}
</ul>
),
},
]}
/>
);
}
The Tabs component is interactive (client), but the tab content is server-rendered. No database queries ship to the client.
Data Fetching Patterns
Docker Compose brings up your entire stack with a single command.
Server Component Fetch
// Direct database access (server only)
async function getUser(id) {
return db.user.findUnique({ where: { id } });
}
// Fetch API with caching
async function getPosts() {
const res = await fetch("https://api.example.com/posts", {
next: { revalidate: 3600 }, // Cache for 1 hour
});
return res.json();
}
Client Component Fetch
"use client";
import useSWR from "swr";
function UserProfile({ userId }: { userId }) {
const { data, error, isLoading } = useSWR(
"/api/users/" + userId,
(url) => fetch(url).then((r) => r.json())
);
if (isLoading) return <Skeleton />;
if (error) return <Error message={error.message} />;
return <div>{data.name}</div>;
}
Common Pitfalls
Pitfall 1: Using hooks in Server Components
// WRONG - this will error
export default function Page() {
const [count, setCount] = useState(0); // Error: useState is a Client hook
return <div>{count}</div>;
}
// CORRECT - add "use client" or extract to a Client Component
"use client";
export default function Counter() {
const [count, setCount] = useState(0);
return <button => setCount(count + 1)}>{count}</button>;
}
Pitfall 2: Importing a Server Component into a Client Component
Free Resource
Free Cloud Architecture Checklist
A 47-point checklist covering security, scalability, cost optimization, and disaster recovery for production cloud environments.
// WRONG - ServerComponent becomes a Client Component
"use client";
import { ServerComponent } from "./ServerComponent"; // This is now client!
// CORRECT - pass as children
"use client";
function ClientWrapper({ children }: { children: React.ReactNode }) {
return <div className="interactive">{children}</div>;
}
// In a Server Component parent:
<ClientWrapper>
<ServerComponent /> {/* Stays as Server Component */}
</ClientWrapper>
Pitfall 3: Serialization
Props passed from Server to Client Components must be serializable (JSON). No functions, no classes, no Dates:
// WRONG
<ClientComponent date={new Date()} />
// CORRECT
<ClientComponent dateString={date.toISOString()} />
Server infrastructure: production and staging environments connected via VLAN with offsite backups.
Decision Flowchart
- Does it need onClick, onChange, or any event handler? -> Client Component
- Does it use useState, useEffect, or useContext? -> Client Component
- Does it use browser APIs (window, localStorage)? -> Client Component
- Does it only display data? -> Server Component
- Does it fetch data from a database/API? -> Server Component
- Does it use sensitive environment variables? -> Server Component
When in doubt, start with Server Components and only add "use client" when you need interactivity.
At TechSaaS, our company website (www.techsaas.cloud) uses Next.js 15 with the App Router. Most pages are Server Components that fetch from Directus CMS. Only interactive elements like search bars, filter dropdowns, and language selectors are Client Components.
Need help with Next.js architecture? Contact [email protected].
Related Service
Cloud Solutions
Let our experts help you build the right technology strategy for your business.
Need help with tutorials?
TechSaaS provides expert consulting and managed services for cloud infrastructure, DevOps, and AI/ML operations.
We Will Build You a Demo Site — For Free
Like it? Pay us. Do not like it? Walk away, zero complaints. You will spend way less than hiring developers or any agency.
No spam. No contracts. Just a free demo.