Server and Client Components in App Router
The App Router runs React components on the server by default. Server Components execute only on the server, fetch data directly from databases, use API keys safely, and send zero JavaScript to the browser—dramatically improving performance. Client Components (marked with "use client") run in the browser, use React hooks, and interact with the DOM. Understanding this distinction is critical for building efficient Next.js apps. This article covers when to use each and how they interact.
What Are Server Components?
Server Components are React components that render exclusively on the server. They don't support React hooks (like useState or useEffect), don't execute browser APIs (like localStorage or window), and send no JavaScript bundle to the client—only the rendered HTML. They're ideal for fetching data, connecting to databases, and hiding secrets.
A Server Component is any component file without a "use client" directive. Here's an example:
// app/posts/page.tsx (Server Component by default)
import db from "@/lib/database";
export default async function PostsPage() {
const posts = await db.query("SELECT * FROM posts");
return (
<div>
<h1>Posts</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
Notice:
- The component is
async(server-only feature) - It directly queries the database
- No API key or secret is exposed to the browser
- The resulting HTML has no JavaScript overhead
What Are Client Components?
Client Components (marked with "use client") are traditional React components that run in the browser. They support hooks, can access browser APIs, and handle interactivity. Mark a component with "use client" to opt into client-side rendering:
// app/counter.tsx
"use client";
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
This component:
- Uses
useState(client-only hook) - Handles clicks interactively
- Runs JavaScript in the browser
- Can't access databases or secrets
When to Use Each
| Feature | Server Component | Client Component |
|---|---|---|
| Fetch data | Yes | No (use API) |
| Access databases | Yes | No |
| Use API keys | Yes (safe) | No (exposed) |
| Use hooks | No | Yes |
Access window | No | Yes |
| Handle click events | No | Yes |
| Read browser storage | No | Yes |
Use Server Components for:
- Fetching and transforming data
- Accessing databases or private APIs
- Storing secrets (API keys, tokens)
- Heavy computations (rendering markdown, templates)
Use Client Components for:
- Interactivity (buttons, forms, clicks)
- React hooks (
useState,useEffect, custom hooks) - Browser APIs (
localStorage,geolocation,Notification) - Event listeners and real-time updates
Composing Server and Client Components
Servers and Client Components work together. A Server Component can import a Client Component, fetch data, and pass it as a prop:
// app/blog/page.tsx (Server Component)
import PostCard from "./post-card";
import db from "@/lib/database";
export default async function BlogPage() {
const posts = await db.query("SELECT * FROM posts");
return (
<div>
<h1>Blog</h1>
{posts.map((post) => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
// app/blog/post-card.tsx (Client Component)
"use client";
import { useState } from "react";
export default function PostCard({ post }: { post: any }) {
const [liked, setLiked] = useState(false);
return (
<article className="border rounded p-4 mb-4">
<h2 className="text-xl font-bold">{post.title}</h2>
<p>{post.excerpt}</p>
<button
onClick={() => setLiked(!liked)}
className="mt-4 bg-blue-600 text-white px-4 py-2 rounded"
>
{liked ? "Unlike" : "Like"}
</button>
</article>
);
}
The BlogPage Server Component fetches posts from the database and passes them to PostCard, a Client Component that handles likes. This pattern combines efficiency with interactivity.
The "Use Client" Boundary
When you mark a component with "use client", that component and all its children become client-side—even if the children don't use hooks. This is called the client boundary. Keep client boundaries at the leaves (interactive elements) to minimize the JavaScript sent to the browser.
Bad: Marking the entire page as client:
// app/page.tsx
"use client";
import { useState } from "react";
import db from "@/lib/database"; // ❌ Can't use in client component!
export default function HomePage() {
// ...
}
Good: Marking only the interactive button as client:
// app/page.tsx (Server Component)
import LikeButton from "./like-button";
export default async function HomePage() {
const data = await db.query("SELECT * FROM posts");
return (
<div>
<h1>Home</h1>
{data.map((item) => (
<div key={item.id}>
<h2>{item.title}</h2>
<LikeButton itemId={item.id} />
</div>
))}
</div>
);
}
// app/like-button.tsx (Client Component)
"use client";
import { useState } from "react";
export default function LikeButton({ itemId }: { itemId: number }) {
const [liked, setLiked] = useState(false);
return (
<button onClick={() => setLiked(!liked)}>
{liked ? "Unlike" : "Like"}
</button>
);
}
Now most of the page is server-rendered (fast, data-driven), and only the button is client-side (interactive).
Fetching Data in Client Components
Client Components can't fetch data during render. Instead, fetch data from Server Components or use Client-side APIs:
Option 1: Fetch from Server Component (Recommended)
// app/products/page.tsx (Server)
import ProductList from "./product-list";
async function getProducts() {
const res = await fetch("https://api.example.com/products");
return res.json();
}
export default async function ProductsPage() {
const products = await getProducts();
return <ProductList initialProducts={products} />;
}
// app/products/product-list.tsx (Client)
"use client";
import { useState } from "react";
export default function ProductList({
initialProducts,
}: {
initialProducts: any[];
}) {
const [products, setProducts] = useState(initialProducts);
const [filter, setFilter] = useState("");
const filtered = products.filter((p) =>
p.name.toLowerCase().includes(filter.toLowerCase())
);
return (
<div>
<input
type="text"
placeholder="Filter..."
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
<ul>
{filtered.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
</div>
);
}
Option 2: Fetch in useEffect (for dynamic updates)
// app/search.tsx (Client)
"use client";
import { useState, useEffect } from "react";
export default function Search() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!query) {
setResults([]);
return;
}
setLoading(true);
fetch(`/api/search?q=${query}`)
.then((res) => res.json())
.then((data) => {
setResults(data.results);
setLoading(false);
});
}, [query]);
return (
<div>
<input
type="text"
placeholder="Search..."
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
{loading && <p>Loading...</p>}
<ul>
{results.map((result) => (
<li key={result.id}>{result.title}</li>
))}
</ul>
</div>
);
}
Common Patterns
Pattern 1: Server Fetches, Client Displays
// Server fetches data, Client displays with interactivity
export default async function Page() {
const data = await fetchData();
return <ClientComponent data={data} />;
}
Pattern 2: Form in Client, Handler in Server
// app/submit-form.tsx
"use client";
import { createPost } from "@/app/actions";
export default function SubmitForm() {
return (
<form action={createPost}>
<input name="title" placeholder="Title" />
<button type="submit">Create</button>
</form>
);
}
// app/actions.ts
"use server";
export async function createPost(formData: FormData) {
const title = formData.get("title");
await db.insert({ title });
redirect("/posts");
}
Key Takeaways
- Server Components (default) run on the server only, fetch data directly, and send no JavaScript. Use for data fetching and logic.
- Client Components (
"use client") run in the browser, support hooks, and handle interactivity. - Mark boundaries strategically—put
"use client"only on interactive components to minimize client-side JavaScript. - Server Components can import Client Components and pass props; Client Components can't import Server Components.
- Fetch in Server Components, pass data as props to Client Components, or fetch in
useEffectfor dynamic updates.
Frequently Asked Questions
Can Server Components use async/await?
Yes. Server Components are inherently async; you can use async/await directly in the component function without extra setup.
Can a Client Component import a Server Component?
No. Client Components can't import Server Components because the server component code might reference databases or secrets. The build fails with a clear error. Instead, pass server-rendered content as children or props.
Does "use client" make my entire page client-rendered?
Yes. When you add "use client" to a component, that component and all its children are client-rendered. To keep most of your page server-rendered, put "use client" only on leaf components (buttons, forms, interactive elements).
Why would I ever use Client Components if Server Components are more efficient?
For interactivity. Server Components can't handle clicks, track state, or respond to user input. For a button that increments a counter, you need a Client Component. For a page that fetches and displays data, use a Server Component.
Can I call a Server Component function from a Client Component?
Not directly. But you can use "Server Actions"—functions marked with "use server" that Client Components can call. See the pattern in the FAQ above.
Further Reading
- Next.js Server and Client Components – official comprehensive guide.
- React Server Components RFC – deep understanding of the technology.
- Next.js Server Actions – calling server code from clients safely.