Server Components
Server Components are the default in App Router. They run on the server, never ship JavaScript to the browser, and can use async/await directly for data fetching. No useEffect, no useState, no client-side data loading boilerplate.
The Default is Server
Every component in app/ is a Server Component unless you add "use client" at the top. This is React Server Components (RSC), not a Next.js invention, but Next.js was the first framework to ship it as the default.
// app/dashboard/page.tsx - this is a Server Component
// No "use client" directive = runs on the server
export default async function Dashboard() {
const stats = await db.query("SELECT * FROM stats");
return (
<div>
<h1>Dashboard</h1>
<p>Total users: {stats.totalUsers}</p>
<p>Revenue: ${stats.revenue}</p>
</div>
);
}
This component:
- Runs on the server only
- Ships zero JavaScript to the client
- Can directly access databases, file systems, env vars
- Can use
async/awaitat the component level
Data Fetching
Forget useEffect + useState + loading states. In Server Components, you just await:
// Server Component - direct async data fetching
async function UserProfile({ userId }: { userId: string }) {
const user = await db.user.findUnique({ where: { id: userId } });
const posts = await db.post.findMany({ where: { authorId: userId } });
return (
<div>
<h2>{user.name}</h2>
<p>{user.bio}</p>
<PostList posts={posts} />
</div>
);
}
Compare with the old client-side pattern:
// Client Component - the old way (still needed for interactive data)
"use client";
import { useEffect, useState } from "react";
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then((res) => res.json())
.then((data) => { setUser(data); setLoading(false); });
}, [userId]);
if (loading) return <Spinner />;
return <div>{user.name}</div>;
}
Tip: Server Components eliminate waterfalls. Multiple
awaitcalls in a component run on the server (fast, close to the data source), and only the final HTML streams to the client.
”use client” Boundary
Add "use client" when a component needs browser APIs, state, or event handlers:
"use client";
import { useState } from "react";
export function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false);
return (
<button onClick={() => setLiked(!liked)}>
{liked ? "Liked" : "Like"}
</button>
);
}
The Boundary Rule
"use client" creates a boundary. Everything imported by a Client Component also becomes a Client Component, even without the directive.
Server Component (page.tsx)
-> Server Component (PostContent) -- stays on server
-> Client Component (LikeButton) -- "use client"
-> utility function (helpers.ts) -- becomes client-side too
Gotcha: The most common mistake is putting
"use client"at the top of a page or layout. This forces everything inside it to be client-side, killing all Server Component benefits. Push"use client"to the smallest leaf components.
Composition Pattern
The key pattern: Server Components can pass data to Client Components as props, including other Server Components as children.
// app/post/[id]/page.tsx (Server Component)
import { LikeButton } from "./like-button"; // Client Component
import { CommentList } from "./comment-list"; // Server Component
export default async function PostPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const post = await getPost(id);
const comments = await getComments(id);
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
<LikeButton postId={id} initialCount={post.likes} />
<CommentList comments={comments} />
</article>
);
}
// like-button.tsx (Client Component - only this ships JS)
"use client";
import { useState } from "react";
export function LikeButton({ postId, initialCount }: { postId: string; initialCount: number }) {
const [count, setCount] = useState(initialCount);
async function handleLike() {
setCount((c) => c + 1);
await fetch(`/api/posts/${postId}/like`, { method: "POST" });
}
return <button onClick={handleLike}>Likes: {count}</button>;
}
What You Can and Cannot Do
| Server Component | Client Component | |
|---|---|---|
async/await in component | Yes | No |
useState, useEffect | No | Yes |
Event handlers (onClick) | No | Yes |
| Access database/filesystem | Yes | No |
| Read env vars (non-NEXT_PUBLIC_) | Yes | No |
| Import Server Components | Yes | No (but can receive as children) |
| Import Client Components | Yes | Yes |
Serialization Boundary
Props passed from Server to Client Components must be serializable: strings, numbers, booleans, arrays, plain objects, Dates, Maps, Sets, and special React types like JSX (as children). You cannot pass functions, class instances, or Symbols.
// This works
<ClientComponent data={{ name: "Alice", count: 42 }} />
// This does NOT work - functions are not serializable
<ClientComponent onSubmit={async (data) => await save(data)} />
Tip: If you need to pass a function-like behavior from server to client, use a Server Action instead. Server Actions are serializable references to server-side functions.
Third-Party Libraries
Most React UI libraries (date pickers, modals, charts) need useState or useEffect. Wrap them in a Client Component boundary:
// components/chart-wrapper.tsx
"use client";
import { BarChart } from "some-chart-library";
export function ChartWrapper({ data }: { data: ChartData[] }) {
return <BarChart data={data} />;
}
// app/analytics/page.tsx (Server Component)
import { ChartWrapper } from "@/components/chart-wrapper";
export default async function Analytics() {
const data = await getAnalyticsData(); // fetch on server
return <ChartWrapper data={data} />; // pass to client for rendering
}
Next: App Router | Caching