Server Components - Run React components on the server with zero client-side JavaScript
Recipe
// 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.
Working Example
// 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:
- Server Component fetching data with direct database/API access
Suspensestreaming a slow server component- Passing serializable data (plain objects, arrays, strings, numbers) from server to client
- Client Components using state and event handlers
- The
"use client"boundary directive
Deep Dive
How It Works
- Server Components execute only on the server during rendering. Their output is a serialized React tree (RSC payload) that is streamed to the client.
- The RSC payload is not HTML -- it is a compact binary/JSON-like format that React on the client can reconcile with the existing DOM.
- Components are Server Components by default. A file must include
"use client"at the top to opt into client-side rendering. - Server Components can
importClient Components, but Client Components cannot import Server Components. Instead, pass Server Components aschildrenor other JSX props. - Props crossing the server-client boundary must be serializable: strings, numbers, booleans, null, arrays, plain objects, Dates,
Map,Set,FormData, typed arrays,Promise(withuse()), server actions, and JSX elements. Functions (except server actions), classes, and DOM nodes are not serializable. - Server Components can be
async-- they can useawaitdirectly in the function body. This is not allowed in Client Components. - Because Server Components ship zero JavaScript to the browser, large dependencies used only for rendering (e.g., syntax highlighters, markdown parsers) have no client bundle impact.
Variations
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>;
}TypeScript Notes
- Async Server Components return
Promise<JSX.Element>. TypeScript handles this automatically with React 19 types. - Use
React.ReactNodefor thechildrenprop when a Client Component wraps Server Component output. - Serializable prop types should avoid functions, class instances, and Symbols.
Gotchas
- Importing a Client Component into a Server Component is fine; the reverse is not -- A Client Component cannot
importa Server Component module. Fix: Pass the Server Component aschildrenor another JSX prop instead of importing it. - Hooks are not allowed in Server Components --
useState,useEffect,useRef, etc. are client-only. Fix: Move interactive logic into a"use client"component. - Closures do not serialize -- You cannot pass a regular function as a prop from server to client. Fix: Use a Server Action (
"use server") instead, which React serializes as an RPC reference. - Third-party libraries may not be RSC-compatible -- Libraries that use
useEffector browser APIs will fail in Server Components. Fix: Import them only inside"use client"files or use a wrapper. - No access to browser APIs --
window,document,localStorageetc. do not exist on the server. Fix: Gate browser-only code behind"use client". - Re-rendering works differently -- Server Components re-execute on the server when the route changes. They do not re-render in response to state changes. Fix: Lift interactive state into Client Components.
Alternatives
| 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 |
FAQs
What makes a component a Server Component vs a Client Component?
- Components are Server Components by default in frameworks that support RSC (e.g., Next.js App Router)
- A file must include
"use client"at the top to opt into client-side rendering - Server Components run only on the server and ship zero JavaScript to the browser
Can a Client Component import a Server Component?
- No. Client Components cannot import Server Component modules
- Instead, pass Server Components as
childrenor other JSX props to Client Components - A Server Component can import Client Components without restrictions
What data types can be passed as props from a Server Component to a Client Component?
- Strings, numbers, booleans, null, arrays, plain objects, Dates,
Map,Set,FormData, typed arrays Promise(consumed withuse()) and server actions are also serializable- Functions (except server actions), class instances, and DOM nodes are not serializable
Can I use useState, useEffect, or other hooks inside a Server Component?
- No. Hooks like
useState,useEffect,useRef, etc. are client-only - Server Components can use
awaitdirectly in the function body instead - Move interactive logic into a
"use client"component
How do you fetch data in parallel in a Server Component?
async function Dashboard() {
const [users, posts, stats] = await Promise.all([
fetchUsers(),
fetchPosts(),
fetchStats(),
]);
return <>{/* render data */}</>;
}What is the RSC payload and how does streaming work?
- The RSC payload is a compact binary/JSON-like format (not HTML) that React on the client reconciles with the existing DOM
- Server Components can be wrapped in
<Suspense>boundaries for streaming -- the fallback shows while the component resolves - This allows the page to be partially interactive before all data has loaded
Gotcha: A third-party library breaks when used in a Server Component. Why?
- Libraries that use
useEffect,useState, or browser APIs (window,document) will fail in Server Components - Import them only inside
"use client"files or create a thin client wrapper component
Gotcha: Why does my Server Component not re-render when client state changes?
- Server Components re-execute on the server only when the route changes
- They do not re-render in response to client-side state changes
- Lift interactive state into Client Components if you need reactivity
How do you pass a promise from a Server Component to a Client Component for deferred loading?
// 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.
How do you type an async Server Component in TypeScript?
- Async Server Components return
Promise<JSX.Element>-- TypeScript handles this automatically with React 19 types - Use
React.ReactNodefor thechildrenprop when a Client Component wraps Server Component output - Serializable prop types should avoid functions, class instances, and Symbols
What is the composition pattern for wrapping Server Components in Client Components?
"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.
Do large dependencies used only in Server Components affect the client bundle?
- No. Because Server Components ship zero JavaScript to the browser, large dependencies like syntax highlighters or markdown parsers have no client bundle impact
- This is a key performance benefit of the RSC architecture
Related
- Overview -- What's new in React 19
- Server Actions -- Mutating data from server functions
- use() Hook -- Consuming promises passed from Server Components
- Asset Loading -- Preloading resources alongside server-rendered content