//
Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
// app/page.tsx (Server Component -- the default in Next.js App Router)
import { db } from "@/lib/db";
import ClientCounter from "./ClientCounter";
export default async function DashboardPage() {
// Direct database access -- this code never reaches the browser
const stats = await db.query("SELECT count(*) FROM orders");
return (
<main>
<h1>Dashboard</h1>
<p>Total orders: {stats.count}</p>
{/* Hand off to a Client Component for interactivity */}
<ClientCounter initialCount={stats.count} />
</main>
);
}// app/ClientCounter.tsx
"use client";
import { useState } from "react";
export default function ClientCounter({ initialCount }: { initialCount: number }) {
const [count, setCount] = useState(initialCount);
return <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>;
}When to reach for this: Use Server Components whenever a component only needs to read data and render HTML -- no event handlers, no state, no browser APIs.
// A product catalog page mixing server and client concerns
// app/products/page.tsx (Server Component)
import { Suspense } from "react";
import { getProducts, getCategories } from "@/lib/api";
import ProductGrid from "./ProductGrid";
import CategoryFilter from "./CategoryFilter";
export default async function ProductsPage() {
const categories = await getCategories();
return (
<div className="flex gap-6">
{/* Client Component for interactive filtering */}
<CategoryFilter categories={categories} />
{/* Server Component with streaming */}
<Suspense fallback={<p>Loading products...</p>}>
<ProductList />
</Suspense>
</div>
);
}
async function ProductList() {
const products = await getProducts();
// ProductGrid is a "use client" component that receives serializable props
return <ProductGrid products={products} />;
}// app/products/CategoryFilter.tsx
"use client";
import { useState, useTransition } from "react";
import { useRouter } from "next/navigation";
type Props = { categories: { id: string; name: string }[] };
export default function CategoryFilter({ categories }: Props) {
const [selected, setSelected] = useState<string | null>(null);
const [isPending, startTransition] = useTransition();
const router = useRouter();
function handleSelect(id: string) {
setSelected(id);
startTransition(() => {
router.push(`/products?category=${id}`);
});
}
return (
<aside>
<h2>Categories</h2>
<ul>
{categories.map((c) => (
<li key={c.id}>
<button
onClick={() => handleSelect(c.id)}
className={selected === c.id ? "font-bold" : ""}
>
{c.name}
</button>
</li>
))}
</ul>
{isPending && <p>Filtering...</p>}
</aside>
);
}// app/products/ProductGrid.tsx
"use client";
type Product = { id: string; name: string; price: number };
export default function ProductGrid({ products }: { products: Product[] }) {
return (
<div className="grid grid-cols-3 gap-4">
{products.map((p) => (
<div key={p.id} className="border p-4 rounded">
<h3>{p.name}</h3>
<p>${p.price.toFixed(2)}</p>
<button onClick={() => alert(`Added ${p.name}`)}>Add to cart</button>
</div>
))}
</div>
);
}What this demonstrates:
Suspense streaming a slow server component"use client" boundary directive"use client" at the top to opt into client-side rendering.import Client Components, but Client Components cannot import Server Components. Instead, pass Server Components as children or other JSX props.Map, Set, FormData, typed arrays, Promise (with use()), server actions, and JSX elements. Functions (except server actions), classes, and DOM nodes are not serializable.async -- they can use await directly in the function body. This is not allowed in Client Components.Async data fetching patterns:
// Pattern 1: Top-level await
async function UserProfile({ userId }: { userId: string }) {
const user = await fetchUser(userId);
return <h1>{user.name}</h1>;
}
// Pattern 2: Parallel data fetching
async function Dashboard() {
const [users, posts, stats] = await Promise.all([
fetchUsers(),
fetchPosts(),
fetchStats(),
]);
return (
<>
<UserList users={users} />
<PostFeed posts={posts} />
<StatsPanel stats={stats} />
</>
);
}
// Pattern 3: Pass promise to client (defer resolution)
async function Page() {
const dataPromise = fetchSlowData(); // do NOT await
return <ClientChart dataPromise={dataPromise} />;
}Composition pattern -- passing Server Components as children:
// This works because children is already-rendered JSX, not an import
"use client";
export function ClientLayout({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(true);
return <div className={open ? "expanded" : "collapsed"}>{children}</div>;
}Promise<JSX.Element>. TypeScript handles this automatically with React 19 types.React.ReactNode for the children prop when a Client Component wraps Server Component output.import a Server Component module. Fix: Pass the Server Component as children or another JSX prop instead of importing it.useState, useEffect, useRef, etc. are client-only. Fix: Move interactive logic into a "use client" component."use server") instead, which React serializes as an RPC reference.useEffect or browser APIs will fail in Server Components. Fix: Import them only inside "use client" files or use a wrapper.window, document, localStorage etc. do not exist on the server. Fix: Gate browser-only code behind "use client".| Approach | When to choose |
|---|---|
| Server Components | Read-only UI, data fetching, large dependencies you want out of the bundle |
| Client Components | Interactive UI with state, effects, or browser APIs |
| Server-side rendering (SSR) without RSC | Pre-React 19 apps, frameworks that do not support RSC yet |
| Static Site Generation (SSG) | Content that rarely changes and can be built at deploy time |
| API routes + client fetch | When you need fine-grained control over caching and data shape |
"use client" at the top to opt into client-side renderingchildren or other JSX props to Client ComponentsMap, Set, FormData, typed arraysPromise (consumed with use()) and server actions are also serializableuseState, useEffect, useRef, etc. are client-onlyawait directly in the function body instead"use client" componentasync function Dashboard() {
const [users, posts, stats] = await Promise.all([
fetchUsers(),
fetchPosts(),
fetchStats(),
]);
return <>{/* render data */}</>;
}<Suspense> boundaries for streaming -- the fallback shows while the component resolvesuseEffect, useState, or browser APIs (window, document) will fail in Server Components"use client" files or create a thin client wrapper component// Server Component -- do NOT await
async function Page() {
const dataPromise = fetchSlowData();
return <ClientChart dataPromise={dataPromise} />;
}The client component uses use(dataPromise) inside a <Suspense> boundary to resolve it.
Promise<JSX.Element> -- TypeScript handles this automatically with React 19 typesReact.ReactNode for the children prop when a Client Component wraps Server Component output"use client";
export function ClientLayout({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(true);
return <div className={open ? "expanded" : "collapsed"}>{children}</div>;
}The children is already-rendered JSX from a Server Component, not an import.
Reviewed by Chris St. John·Last updated Jul 16, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥