Client vs Server Components in Next.js
A deep architectural guide to React Server Components and Client Components in Next.js: execution models, the Flight protocol, composition patterns, and boundaries.

Client vs Server Components in Next.js
The introduction of React Server Components (RSC) within the Next.js App Router represents a structural shift in how full-stack React applications are designed, compiled, and executed. In traditional React single-page applications (SPAs) and early Server-Side Rendering (SSR) implementations, the entire component tree was compiled into JavaScript bundles shipped to the browser. Every utility library, formatting helper, and UI component incurred a cost in network transfer, client-side parse time, and memory overhead.
Modern Next.js splits the component tree across two distinct execution environments: Server Components and Client Components. Rather than treating server-rendering as an all-or-nothing initial HTML pass, the framework treats Server Components as the default computational backbone and Client Components as opt-in interactive leaves.
Understanding this duality requires more than memorizing the 'use client' directive. It demands a clear mental model of how execution contexts differ, how the React Flight protocol streams data across the network boundary, what constraints govern prop serialization, and how to compose server and client code without accidentally bloating client bundles.
1. The Core Duality: Two Execution Environments
In the App Router, every component is a React Server Component by default unless explicitly designated as a Client Component. These two component types fulfill complementary roles in a production application.
React Server Components (RSC): The Default Engine
Server Components execute exclusively on the server—either inside a Node.js runtime or an Edge runtime during request handling, or at build time during static page generation. Their rendered output is translated into a specialized virtual DOM representation rather than executable client-side JavaScript.
Key architectural characteristics include:
- Zero Client Bundle Impact: The code for a Server Component is never sent to the browser. Third-party packages imported exclusively within Server Components—such as markdown parsers (
marked), syntax highlighters (shiki), mathematical processors, or database drivers—do not add a single byte to the client JavaScript payload. - Direct Backend Resource Access: Because Server Components run in a secure server environment, they can query databases directly, read from the local file system, call internal microservices over private networks, and access sensitive environment variables without exposing API keys or connection strings to the client.
- Inherent Architectural Constraints: Because Server Components never run in a browser runtime, they cannot access browser-specific APIs (
window,document,navigator,localStorage). Furthermore, they cannot hold interactive state or react to user events: hooks such asuseState,useReducer,useEffect,useLayoutEffect, anduseRefare disallowed, as are DOM event handlers likeonClickandonChange.
// app/products/page.tsx (Server Component by default)
import { db } from "@/lib/db";
export default async function ProductsPage() {
// Direct database query on the server — zero client bundle impact
const products = await db.query("SELECT id, name, price_cents FROM products");
return (
<main className="p-6">
<h1 className="text-2xl font-bold">Catalog</h1>
<ul className="mt-4 space-y-2">
{products.map((item) => (
<li key={item.id} className="border p-4 rounded">
<span>{item.name}</span> - ${(item.price_cents / 100).toFixed(2)}
</li>
))}
</ul>
</main>
);
}
Client Components ('use client'): Islands of Interactivity
A Client Component is declared by placing the 'use client' directive at the very top of a file, before any import statements. The directive does not mean "render this component only in the browser." Instead, it defines a boundary marker instructing the compiler that this module and all modules it imports belong to the client bundle.
Demystifying the Execution Model
A common misconception is that Client Components run only on the client. In reality, Client Components follow a dual-phase execution lifecycle:
- Server Pre-Rendering (SSR): During the initial full-page HTTP request (or static site generation), Next.js executes Client Components on the server to generate an initial static HTML snapshot. This ensures fast First Contentful Paint (FCP) and guarantees that search engine crawlers receive complete HTML markup.
- Client Hydration: Once the browser downloads the page's HTML and the associated JavaScript chunks, React initializes the component tree in memory, attaches event listeners to the pre-rendered HTML DOM, and establishes active state management.
- Subsequent Client Transitions: During client-side navigation (e.g., clicking a Next.js
Link), Client Components render entirely on the client without an additional server-side HTML rendering pass.
Client Components are necessary whenever your UI requires:
- State and Lifecycle Hooks: Managing local UI state with
useState,useReducer,useRef, or executing side-effects withuseEffectanduseLayoutEffect. - Event Listeners: Handling user input via
onClick,onChange,onSubmit,onKeyDown, or drag-and-drop events. - Browser-Only APIs: Accessing window dimensions, geolocation,
localStorage,sessionStorage,matchMedia,IntersectionObserver, or HTML<canvas>elements. - Browser-Dependent Libraries: Integrating third-party animation libraries (e.g., Framer Motion) or client-side chart engines that rely directly on DOM manipulation.
"use client";
// components/counter.tsx
import { useState } from "react";
export function Counter({ initialCount = 0 }: { initialCount?: number }) {
const [count, setCount] = useState(initialCount);
return (
<div className="flex items-center gap-3">
<button
onClick={() => setCount((prev) => prev - 1)}
className="px-3 py-1 bg-gray-200 rounded"
aria-label="Decrement count"
>
-
</button>
<span className="font-mono text-lg">{count}</span>
<button
onClick={() => setCount((prev) => prev + 1)}
className="px-3 py-1 bg-gray-200 rounded"
aria-label="Increment count"
>
+
</button>
</div>
);
}
2. Server vs. Client Capabilities Comparison
To evaluate whether a component belongs on the server or the client, review their technical constraints and capabilities side by side:
| Capability / Characteristic | React Server Component (RSC) | Client Component ('use client') |
| :--- | :--- | :--- |
| Default in App Router | Yes | No (requires explicit directive) |
| Execution Environment | Server only (Node.js / Edge) | Server (pre-rendering) and Browser (hydration & navigation) |
| Shipped in Client JS Bundle | No (0 bytes of component logic) | Yes (component code and imported client dependencies) |
| Direct Database / Filesystem Access | Yes | No |
| Private Server Secrets (API keys, DB URLs)| Yes (accessible via process.env) | No (must use NEXT_PUBLIC_ or fetch via APIs / Server Actions) |
| State Hooks (useState, useReducer) | No | Yes |
| Lifecycle Hooks (useEffect) | No | Yes |
| Browser DOM Event Listeners (onClick) | No | Yes |
| Browser Globals (window, localStorage)| No | Yes (guarded inside useEffect or after mount) |
| Data Fetching Paradigm | async/await directly in component body | Fetch inside Server Actions, Route Handlers, or custom hooks |
3. The Network Serialization Boundary & React Flight Protocol
When a Next.js application serves a page, it does not send raw JSX or HTML alone. It uses an internal streaming wire format known as the React Flight Protocol.
The Flight Wire Format
When a request arrives, Next.js renders the Server Component tree on the server into a stream of serialized UI chunks. This Flight stream contains:
- Rendered Server Component Output: HTML tags, text nodes, and their computed attributes.
- Client Component Module References: Placeholders (often formatted with
$Land a module ID) identifying which client-side JavaScript chunk needs to be loaded by the browser. - Serialized Props: The exact properties passed from Server Components to Client Components across the network boundary.
Server Rendering Pipeline:
┌────────────────────────────────────────────────────────┐
│ Server Component Tree │
│ ├── <Layout> │
│ │ ├── <Sidebar /> (RSC -> Evaluated to DOM tags) │
│ │ └── <ClientModal data={...}> │
│ │ └── <ServerDetails /> (Slot / Child) │
└──────────────────────────┬─────────────────────────────┘
│
▼ React Flight Serializer
┌────────────────────────────────────────────────────────┐
│ React Flight Wire Stream │
│ - Streamed JSON-like descriptors │
│ - Module references: { id: "chunk-modal.js", name: "Modal" }
│ - Serialized props payload for <ClientModal> │
│ - Rendered virtual DOM nodes for <ServerDetails> │
└──────────────────────────┬─────────────────────────────┘
│ HTTP Chunked Transfer
▼
┌────────────────────────────────────────────────────────┐
│ Browser Client Runtime │
│ 1. Deserializes Flight stream │
│ 2. Fetches referenced client chunks (chunk-modal.js) │
│ 3. Reconciles DOM and hydrates interactive listeners │
└────────────────────────────────────────────────────────┘
Because the server streams this representation over HTTP, the browser can parse and display Server Component UI incrementally before all downstream client JavaScript chunks have finished downloading.
Strict Serialization Constraints
Because props passed from a Server Component to a Client Component cross an HTTP network boundary, they must be serializable by React's Flight protocol.
Supported Data Types Across the Boundary
- Primitives:
string,number,boolean,null,undefined,bigint. - Plain Data Structures: Standard JavaScript objects (
{}), arrays ([]),Map, andSet. - Date Instances: Standard
Dateobjects (React serializes and re-instantiates them). - Typed Arrays:
Uint8Array,Int32Array, and other array buffers. - Promises: React can stream unresolved promises across the boundary, which the Client Component consumes using the React 19
use()hook. - Server Actions: Asynchronous functions marked with the
'use server'directive can be passed as function references across the boundary.
Prohibited Data Types Across the Boundary
- Arbitrary Client Functions: Standard functions or closures cannot be passed as props (e.g.,
<ClientButton onClick={() => console.log('hello')} />inside a Server Component throws a serialization error). - Class Instances with Methods: Passing class instances strips their prototype methods during serialization, resulting in plain objects or runtime errors.
- Symbols: Standard
Symbol()values cannot be serialized across the wire. - DOM Elements: Browser DOM nodes cannot be created on the server or transmitted.
// app/users/page.tsx (Server Component)
import { UserProfileClient } from "@/components/user-profile-client";
class UserAccount {
constructor(public id: string, public name: string) {}
formatDisplayName() {
return `User: ${this.name}`;
}
}
export default function UsersPage() {
const account = new UserAccount("u1", "Alex Rivera");
return (
// ❌ ERROR: Class instances with methods cannot be passed across the boundary
// <UserProfileClient account={account} />
// ✅ CORRECT: Pass plain serializable data
<UserProfileClient
id={account.id}
displayName={account.formatDisplayName()}
/>
);
}
4. Architectural Composition Patterns
Structuring a large Next.js codebase requires clean boundaries between server and client components. Four primary patterns prevent architectural degradation and bundle bloat.
Pattern 1: Leaf-Node Interactivity
The golden rule of React Server Component architecture is: push the 'use client' directive as far down the component tree as possible.
When an entire page or large container is marked with 'use client', every subcomponent, layout element, and utility imported inside that file is forced into the client bundle—even if ninety percent of the markup is purely informational.
Anti-Pattern: Top-Heavy Client Boundary
┌────────────────────────────────────────┐
│ <ProductPage> ('use client') │
│ ├── <ProductHero /> [In Bundle] │
│ ├── <ProductSpecsTable /> [In Bundle] │
│ ├── <ReturnPolicy /> [In Bundle] │
│ └── <AddToCartButton /> [In Bundle] │
└────────────────────────────────────────┘
Recommended: Leaf-Node Interactivity
┌────────────────────────────────────────┐
│ <ProductPage> (Server Component) │
│ ├── <ProductHero /> [0 KB JS] │
│ ├── <ProductSpecsTable /> [0 KB JS] │
│ ├── <ReturnPolicy /> [0 KB JS] │
│ └── <AddToCartButton /> ['use client']
└────────────────────────────────────────┘
By isolating interactivity to the <AddToCartButton />, the static product specifications, description, and hero sections remain Server Components, keeping the client bundle minimal and reducing client CPU parse overhead.
Pattern 2: The Interleaving Pattern (Passing Server Components as Children)
A common architectural challenge arises when an interactive Client Component (such as a collapsible sidebar, a modal dialog, or a tabbed container) needs to display server-rendered content.
The "Cascade Poisoning" Anti-Pattern
If you directly import a Server Component into a Client Component file, the compiler treats the imported component as part of the client module graph. It is compiled as a Client Component, executed on the client, and stripped of its ability to query databases or access server secrets directly.
// ❌ ANTI-PATTERN: Direct import poisons the Server Component
"use client";
import { useState } from "react";
import { ServerDataFeed } from "./server-data-feed"; // Converted to Client Component!
export function CollapsibleContainer() {
const [isOpen, setIsOpen] = useState(true);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
{isOpen && <ServerDataFeed />}
</div>
);
}
The Slot Solution: Using the children Prop
To render a Server Component inside a Client Component without converting it into a Client Component, pass the Server Component as a child (or named JSX slot) from a parent Server Component.
Interleaving Component Architecture:
┌────────────────────────────────────────────────────────┐
│ Parent Server Component (e.g., app/dashboard/page.tsx) │
│ │
│ Renders both components on the server: │
│ │
│ <ClientCollapsible> │
│ └── children={<ServerDataFeed />} │
│ </ClientCollapsible> │
└──────────────────────────┬─────────────────────────────┘
│
┌───────────────────┴───────────────────┐
▼ ▼
┌──────────────────────────────┐ ┌──────────────────────────────┐
│ <ClientCollapsible> │ │ <ServerDataFeed> │
│ ('use client') │ │ (Server Component) │
│ Manages local open/close UI │ │ Fetches data on server; │
│ Renders {children} slot │ │ Output serialized to Flight │
└──────────────────────────────┘ └──────────────────────────────┘
Because the parent Server Component resolves <ServerDataFeed /> on the server, the resulting virtual DOM structure is serialized into the Flight stream and passed into <ClientCollapsible> as the children prop. The client component manages toggling without ever importing or bundling <ServerDataFeed />.
Pattern 3: Protecting Backend Secrets with server-only
In large teams, an engineer might inadvertently import a server utility—such as a database query module or private encryption utility—into a Client Component. While Next.js prevents private environment variables (those lacking NEXT_PUBLIC_) from being embedded in client bundles, server-side functions containing sensitive logic could still be exposed.
To prevent this, use the official server-only package:
npm install server-only
Add import 'server-only'; at the top of any module that must never touch the browser:
// lib/auth-tokens.ts
import "server-only";
export function verifyAdminToken(token: string): boolean {
const secret = process.env.ADMIN_SECRET_KEY;
if (!secret) {
throw new Error("ADMIN_SECRET_KEY is not configured on the server");
}
return token === secret;
}
If any file marked with 'use client' (or imported by one) attempts to import lib/auth-tokens.ts, Next.js throws an immediate build-time error:
Failed to compile
./lib/auth-tokens.ts
You're importing a component that needs "server-only". That module is only available on the server.
Pattern 4: Seamless Server Actions Across Boundaries
Server Actions ('use server') allow Client Components to invoke asynchronous server functions directly without manually writing boilerplate REST API endpoints.
When combined with React 19 hooks such as useActionState and useFormStatus, forms maintain full client-side interactivity, pending feedback, and optimistic state while delegating business logic and database writes to the server.
5. Comprehensive Production Implementation
Below is a complete, self-contained implementation demonstrating these architectural patterns working together in a Next.js application.
File 1: Secure Server Data Module
// lib/reviews-db.ts
import "server-only";
export interface Review {
id: string;
productId: string;
author: string;
rating: number;
comment: string;
createdAt: string;
}
// Simulated persistent storage
const REVIEWS_STORE: Review[] = [
{
id: "rev-1",
productId: "prod-100",
author: "Elena Rostova",
rating: 5,
comment: "Exceptional build quality and thermal efficiency.",
createdAt: "2025-10-15T10:30:00Z",
},
{
id: "rev-2",
productId: "prod-100",
author: "Marcus Vance",
rating: 4,
comment: "Reliable performance under sustained load.",
createdAt: "2025-10-16T14:15:00Z",
},
];
export async function getReviewsByProductId(productId: string): Promise<Review[]> {
// Direct backend retrieval — guaranteed never to execute on the client
return REVIEWS_STORE.filter((r) => r.productId === productId);
}
export async function insertReview(
productId: string,
author: string,
rating: number,
comment: string
): Promise<Review> {
const newReview: Review = {
id: `rev-${Date.now()}`,
productId,
author: author.trim(),
rating,
comment: comment.trim(),
createdAt: new Date().toISOString(),
};
REVIEWS_STORE.unshift(newReview);
return newReview;
}
File 2: Type-Safe Server Action
// app/actions/submit-review.ts
"use server";
import { insertReview } from "@/lib/reviews-db";
import { revalidatePath } from "next/cache";
export interface FormActionState {
success: boolean;
message: string;
errors?: {
author?: string;
rating?: string;
comment?: string;
};
}
export async function submitReviewAction(
prevState: FormActionState,
formData: FormData
): Promise<FormActionState> {
const productId = formData.get("productId") as string;
const author = formData.get("author") as string;
const ratingRaw = formData.get("rating") as string;
const comment = formData.get("comment") as string;
const errors: FormActionState["errors"] = {};
if (!author || author.trim().length < 2) {
errors.author = "Author name must be at least 2 characters.";
}
const rating = parseInt(ratingRaw, 10);
if (isNaN(rating) || rating < 1 || rating > 5) {
errors.rating = "Rating must be an integer between 1 and 5.";
}
if (!comment || comment.trim().length < 10) {
errors.comment = "Comment must contain at least 10 characters.";
}
if (Object.keys(errors).length > 0) {
return {
success: false,
message: "Validation failed. Please correct the highlighted fields.",
errors,
};
}
try {
await insertReview(productId, author, rating, comment);
revalidatePath(`/products/${productId}`);
return {
success: true,
message: "Review successfully submitted and published.",
};
} catch (error) {
return {
success: false,
message: "An unexpected server error occurred while persisting the review.",
};
}
}
File 3: Interactive Client Slot Wrapper
// components/collapsible-section.tsx
"use client";
import { useState, type ReactNode } from "react";
interface CollapsibleSectionProps {
title: string;
defaultExpanded?: boolean;
children: ReactNode; // Server Component slot
}
export function CollapsibleSection({
title,
defaultExpanded = true,
children,
}: CollapsibleSectionProps) {
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
return (
<section className="border border-gray-200 rounded-lg overflow-hidden my-4">
<header className="bg-gray-50 px-4 py-3 flex items-center justify-between">
<h2 className="text-lg font-semibold text-gray-800">{title}</h2>
<button
onClick={() => setIsExpanded((prev) => !prev)}
className="text-sm px-3 py-1 bg-white border border-gray-300 rounded hover:bg-gray-100 transition-colors"
aria-expanded={isExpanded}
>
{isExpanded ? "Collapse" : "Expand"}
</button>
</header>
{isExpanded && <div className="p-4 bg-white">{children}</div>}
</section>
);
}
File 4: Interactive Client Form Using React 19 Hooks
// components/review-form.tsx
"use client";
import { useActionState } from "react";
import { useFormStatus } from "react-dom";
import { submitReviewAction, type FormActionState } from "@/app/actions/submit-review";
const initialState: FormActionState = {
success: false,
message: "",
};
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button
type="submit"
disabled={pending}
className="px-4 py-2 bg-blue-600 text-white rounded font-medium disabled:opacity-50 hover:bg-blue-700 transition-colors"
>
{pending ? "Submitting Review..." : "Post Review"}
</button>
);
}
export function ReviewForm({ productId }: { productId: string }) {
const [state, formAction] = useActionState(submitReviewAction, initialState);
return (
<form action={formAction} className="space-y-4 max-w-lg mt-6 border p-4 rounded-lg bg-gray-50">
<input type="hidden" name="productId" value={productId} />
{state.message && (
<div
role="alert"
className={`p-3 rounded text-sm ${
state.success ? "bg-green-100 text-green-800" : "bg-red-100 text-red-800"
}`}
>
{state.message}
</div>
)}
<div>
<label htmlFor="author" className="block text-sm font-medium text-gray-700">
Your Name
</label>
<input
id="author"
name="author"
type="text"
className="mt-1 block w-full rounded border border-gray-300 p-2 text-sm"
required
/>
{state.errors?.author && (
<p className="mt-1 text-xs text-red-600">{state.errors.author}</p>
)}
</div>
<div>
<label htmlFor="rating" className="block text-sm font-medium text-gray-700">
Rating (1-5)
</label>
<select
id="rating"
name="rating"
className="mt-1 block w-full rounded border border-gray-300 p-2 text-sm bg-white"
defaultValue="5"
>
<option value="5">5 - Excellent</option>
<option value="4">4 - Good</option>
<option value="3">3 - Average</option>
<option value="2">2 - Below Expectations</option>
<option value="1">1 - Poor</option>
</select>
{state.errors?.rating && (
<p className="mt-1 text-xs text-red-600">{state.errors.rating}</p>
)}
</div>
<div>
<label htmlFor="comment" className="block text-sm font-medium text-gray-700">
Comments
</label>
<textarea
id="comment"
name="comment"
rows={3}
className="mt-1 block w-full rounded border border-gray-300 p-2 text-sm"
required
/>
{state.errors?.comment && (
<p className="mt-1 text-xs text-red-600">{state.errors.comment}</p>
)}
</div>
<SubmitButton />
</form>
);
}
File 5: Root Server Component Page Composing the Architecture
// app/products/[id]/page.tsx
import { getReviewsByProductId } from "@/lib/reviews-db";
import { CollapsibleSection } from "@/components/collapsible-section";
import { ReviewForm } from "@/components/review-form";
import { notFound } from "next/navigation";
interface PageProps {
params: Promise<{ id: string }>;
}
export default async function ProductPage({ params }: PageProps) {
// In Next.js 15+, dynamic route parameters are resolved asynchronously
const { id } = await params;
// Direct backend query in Server Component
const reviews = await getReviewsByProductId(id);
if (!id) {
notFound();
}
return (
<main className="max-w-4xl mx-auto p-8">
<header className="mb-6">
<h1 className="text-3xl font-bold tracking-tight">Enterprise Server Blade {id}</h1>
<p className="text-gray-600 mt-1">Status: In Stock • High-Density Compute Node</p>
</header>
{/*
The CollapsibleSection is a Client Component ('use client').
The reviews list is passed as the `children` slot, rendered on the server!
*/}
<CollapsibleSection title={`Customer Reviews (${reviews.length})`}>
{reviews.length === 0 ? (
<p className="text-gray-500 italic">No reviews recorded for this configuration yet.</p>
) : (
<div className="space-y-3">
{reviews.map((rev) => (
<article key={rev.id} className="border-b pb-3 last:border-none">
<div className="flex items-center justify-between text-sm">
<span className="font-semibold text-gray-900">{rev.author}</span>
<span className="text-yellow-600">{"★".repeat(rev.rating)}</span>
</div>
<p className="text-sm text-gray-700 mt-1">{rev.comment}</p>
<time className="text-xs text-gray-400 block mt-1">
{new Date(rev.createdAt).toLocaleDateString()}
</time>
</article>
))}
</div>
)}
</CollapsibleSection>
{/* Leaf-Node Client Component managing interactive form submission */}
<section className="mt-8">
<h3 className="text-xl font-semibold">Submit a Verified Review</h3>
<ReviewForm productId={id} />
</section>
</main>
);
}
6. Component Selection Decision Matrix
When designing a new component, consult this decision matrix to determine its optimal placement:
| Requirement / Scenario | Recommended Component Type | Architectural Rationale |
| :--- | :--- | :--- |
| Data Fetching from Database or ORM | Server Component | Direct queries eliminate latency and network hops; keeps connection credentials off the client. |
| Static Layout, Header, or Footer | Server Component | Emits pure HTML; introduces 0 bytes of JavaScript into the client bundle. |
| Interactive Form with Validation State | Client Component ('use client') | Requires event handlers and reactive state (useActionState, useFormStatus). |
| Markdown / Code Syntax Highlighting | Server Component | Parses heavy string operations on the server; prevents large parsing libraries from bloating client bundles. |
| Interactive Modal / Accordion / Dropdown | Client Component (accepting Server children) | State controls visibility; passing server content via children avoids bundling the body content. |
| SEO Metadata Generation (generateMetadata) | Server-Only Export | Next.js requires metadata functions to run exclusively on the server. |
| Browser Window / Viewport Monitoring | Client Component ('use client') | Depends directly on browser globals (window.innerWidth, matchMedia). |
| Third-Party Interactive Canvas / WebGL | Client Component ('use client') | Requires direct DOM node references (useRef) and client rendering contexts. |
7. Common Production Anti-Patterns
Anti-Pattern 1: Marking Pages or Layouts with 'use client'
Placing 'use client' at app/layout.tsx or app/dashboard/page.tsx converts the entire subtree into Client Components by default.
Consequences: Every utility, data transformer, and UI element imported anywhere in the view tree is bundled into client JavaScript. Server-only features such as direct database queries and asynchronous component bodies are disabled.
Resolution: Keep route layouts and page files as Server Components. Extract interactive controls (search inputs, filters, action buttons) into dedicated leaf-node files under a components/ directory.
Anti-Pattern 2: Passing Non-Sanitized Database Models Across Boundaries
Passing full database records (such as ORM entity instances) directly to Client Components exposes internal fields across the network.
Consequences: The Flight protocol serializes every property on the object. If your database entity contains passwordHash, internalAuditNotes, or stripeCustomerId, those values are transmitted in the clear inside the Flight HTTP response.
Resolution: Sanitize or map data in the Server Component before passing it as props, transmitting only the specific fields the client interface requires.
// ❌ ANTI-PATTERN: Leaks full database record into Flight wire stream
export default async function UserCard({ id }: { id: string }) {
const user = await db.users.findUnique({ where: { id } });
return <ClientCard user={user} />; // Contains passwordHash, billingId, etc.
}
// ✅ RECOMMENDED: Project explicit public properties
export default async function UserCard({ id }: { id: string }) {
const user = await db.users.findUnique({ where: { id } });
return (
<ClientCard
id={user.id}
name={user.name}
avatarUrl={user.avatarUrl}
/>
);
}
Anti-Pattern 3: Referencing Browser Globals During Server Pre-Rendering
Calling window, document, or localStorage directly in the module body or render cycle of a Client Component causes runtime crashes during Server-Side Rendering (SSR).
// ❌ ANTI-PATTERN: Crashes during SSR because `window` is undefined
"use client";
export function ScreenSizeBadge() {
const width = window.innerWidth; // ReferenceError: window is not defined
return <span>Width: {width}px</span>;
}
// ✅ RECOMMENDED: Guard browser APIs inside useEffect or event handlers
"use client";
import { useState, useEffect } from "react";
export function ScreenSizeBadge() {
const [width, setWidth] = useState<number | null>(null);
useEffect(() => {
// useEffect runs exclusively in the browser post-hydration
setWidth(window.innerWidth);
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
if (width === null) {
return <span>Measuring...</span>;
}
return <span>Width: {width}px</span>;
}
Anti-Pattern 4: Sequential Client-Side Request Waterfalls
Converting components to Client Components and fetching data with useEffect introduces client-side network waterfalls, where the browser must download HTML, download JavaScript, execute React, and then initiate an HTTP request over a high-latency mobile connection.
Resolution: Fetch data in parent Server Components in parallel using standard Promise.all, then pass the resolved data down to Client Components or render the data directly in server markup.
8. Production Verification Checklist
Before merging component architecture changes into production, execute this review checklist:
- [ ] Directive Placement: Are
'use client'directives placed strictly at leaf nodes rather than page or layout files? - [ ] Bundle Inspection: Have you verified with a bundle analyzer (e.g.,
@next/bundle-analyzer) that server utilities and heavy libraries are not leaking into client chunks? - [ ] Secret Isolation: Do modules containing database queries or API tokens include
import 'server-only';? - [ ] Prop Serialization: Are all props crossing from Server to Client Components serializable (plain objects, primitives, dates, or Server Actions)?
- [ ] Data Minimization: Are database records mapped to concise DTOs before being passed as props, ensuring no internal flags or secrets are included in the Flight stream?
- [ ] Interleaving Verification: Are large static or server-dependent subtrees passed via
childrenrather than imported directly into Client Components? - [ ] SSR Resilience: Are all browser globals (
window,localStorage) in Client Components properly guarded insideuseEffector lifecycle hooks? - [ ] Async Route Props: In Next.js 15+, are
paramsandsearchParamsproperly handled as Promises in Server Component pages?
Conclusion
The distinction between Server and Client Components in Next.js is not an arbitrary framework rule—it is a fundamental model for building scalable, high-performance web applications.
By establishing Server Components as the default execution layer, you leverage server proximity to databases, eliminate client bundle bloat, and improve initial page render speeds. By deliberately introducing Client Components only at interactive leaf boundaries, you provide responsive user experiences without penalizing network transfer or client execution time.
Mastering the boundary—enforcing server-only isolation, adhering to serialization contracts, and utilizing slot-based interleaving—ensures that as your application expands, its architecture remains maintainable, secure, and performant.


