Topics
v5.0.0
On this page · sections

Server & Client Components

React Server Components (RSC) are the default in the App Router. Server Components run on the server, ship zero component JS, and await data directly. Client Components ("use client") run in the browser and handle interactivity.

TypeRuns onCan useCannot use
Server ComponentServerasync/await, server APIs, direct DBHooks, events, browser APIs
Client ComponentBrowserHooks, events, browser APIsasync component body, server-only imports

React Server Components (RSC)

Definition

Server Components render on the server and send a serialized payload to the client — not a JS bundle for the component itself.

// Server Component (default) — no directive needed async function ProductList() { const products = await fetchProducts(); return ( <ul> {products.map((p) => ( <li key={p.id}>{p.name}</li> ))} </ul> ); }

Benefits:

Zero component JS shipped for server-only UI Direct server resource access (DB, filesystem, secrets) Automatic request deduplication within a render pass Streaming via Suspense boundaries

Interview: RSC is not SSR alone — it is a component model where server components never hydrate on the client.

The "use client" directive

Definition

Add "use client" at the top of a file to mark it and all its imports as Client Components.

"use client"; import { useState } from "react"; export function AddToCartButton({ productId }: { productId: string }) { const [pending, setPending] = useState(false); return ( <button disabled={pending} onClick={() => addToCart(productId)} > Add to cart </button> ); }

Rules:

'use client' is per-file, not per-function Client Components can import Server Components only as children/slots — not directly Push 'use client' to the leaves — keep as much tree on the server as possible

Real-world example

Root app/layout.tsx wraps all routes. Client providers go in a thin 'use client' child:

// app/providers.tsx "use client"; export function Providers({ children }: { children: React.ReactNode }) { return <ThemeProvider>{children}</ThemeProvider>; } // app/layout.tsx import { Providers } from "./providers"; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <Providers>{children}</Providers> </body> </html> ); }

Interview: Server Components cannot use hooks or event handlers. Client Components cannot be async or call server-only APIs directly.

Component boundaries

Definition

The server/client boundary is per module. Once a file is "use client", everything it imports becomes part of the client bundle unless dynamically passed from the server.

Server Component ├── can import Server Components directly ├── can render Client Components as children └── cannot import Client Component modules at top level and use their exports inline Client Component ├── can import other Client Components └── receives Server Components only via children prop (composition)
// ✅ Server page renders client button import { AddToCartButton } from "./add-to-cart"; // 'use client' file export default async function ProductPage({ id }: { id: string }) { const product = await fetchProduct(id); return ( <div> <h1>{product.name}</h1> <AddToCartButton productId={id} /> </div> ); }
// ❌ Client component cannot import async server component "use client"; import { ProductList } from "./product-list"; // async server component — error

Interview: The boundary is a one-way door. Server → Client via composition; not Client → Server via import.

Serialization & the RSC payload

Definition

Props passed from Server to Client Components must be serializable — JSON-compatible types that cross the wire.

Allowed → string, number, boolean, null, arrays, plain objects, Date (serialized) Allowed → React elements passed as children (special case) Not allowed → functions, class instances, Symbols, Maps/Sets, server-only objects
// ✅ Serializable props <Counter initialCount={0} label="Items" tags={["sale", "new"]} /> // ❌ Cannot pass a function from server to client as a prop <SearchBox onSearch={(q) => filter(q)} /> // error — functions don't serialize

Workaround — define the callback inside the Client Component or use Server Actions for mutations.

Senior-level usage

Pass IDs and minimal data; let Client Components fetch supplementary client-side data if needed.

// Server <CommentThread postId={post.id} initialComments={comments} /> // Client — receives plain JSON, manages local UI state "use client"; export function CommentThread({ postId, initialComments, }: { postId: string; initialComments: Comment[]; }) { const [comments, setComments] = useState(initialComments); // ... }

Interview: RSC props cross a serialization boundary. Functions, classes, and non-plain objects cannot pass through.

Composition patterns

Definition

The children as slots pattern keeps server data fetching in Server Components while nesting interactive Client Components.

// app/product/[id]/page.tsx — Server Component export default async function ProductPage(props: { params: Promise<{ id: string }>; }) { const { id } = await props.params; const product = await fetchProduct(id); return ( <ProductShell product={product}> <AddToCartButton productId={id} /> <ReviewForm productId={id} /> </ProductShell> ); }
// product-shell.tsx — Server Component export function ProductShell({ product, children, }: { product: Product; children: React.ReactNode; }) { return ( <article> <h1>{product.name}</h1> <p>{product.description}</p> {children} {/* client islands injected here */} </article> ); }

Pattern summary:

Server page fetches data → passes to server layout → slots client leaves as children Provider at root → thin 'use client' wrapper in layout.tsx Shared interactive UI → one 'use client' module, imported by server pages

Interview: Composition via children is the primary way to mix server data with client interactivity without importing server code into client modules.

Server/client data flow

Definition

Data flows in one direction across the boundary:

Server Component → await fetch / DB / unstable_cache → pass serializable props to Client Component → Client Component manages local UI state Client Component → fetch('/api/...') or React Query for client-side data → Server Actions for mutations (form posts, optimistic updates) → router.refresh() to bust client Router Cache after mutations

Server Actions bridge mutations back to the server:

"use server"; export async function addToCart(productId: string) { await db.cart.add(productId); revalidateTag(`cart-${productId}`); }
"use client"; import { addToCart } from "./actions"; export function AddToCartButton({ productId }: { productId: string }) { return ( <button onClick={() => addToCart(productId)}>Add to cart</button> ); }

Real-world example

E-commerce product page data flow:

page.tsx (server) → fetch product, reviews (cached ISR) AddToCartButton → client — calls Server Action StockBadge → client — polls /api/stock every 30s layout.tsx (server) → fetch nav categories (shared, cached)

Interview: Fetch on the server by default. Client fetch is for live/private data that should not block the initial HTML.

Common footguns

Definition

'use client' at layout level → entire subtree becomes client — avoid Passing functions as props → serialization error Importing server code in client → bundle bloat or build error Async Client Component → not supported — fetch in server parent instead Secrets in Client Component → exposed in bundle — keep on server

Audit client bundle impact: every "use client" file and its dependency tree ships to the browser. Fetch data in the server parent; keep client modules as small interactive leaves.

Interview: Minimize the client boundary surface. One misplaced 'use client' in a shared layout can balloon the JS bundle.

Senior Interview Summary

RSC

Default server component model. Zero JS for server-only UI; data fetching colocated with render.

"use client"

Marks a module and its imports as client-side. Required for hooks, events, and browser APIs.

Boundaries

Server can render Client via import or children. Client cannot import Server Components directly.

Serialization

Props crossing the boundary must be JSON-serializable. Use children for server-rendered slots.

Data flow

Server fetch → serialize props → client interactivity. Server Actions for mutations.

Best mental model

Server → "Fetch here, ship HTML + RSC payload." Client → "Interact here, ship JS bundle." Boundary → "One-way: server composes client, not reverse."

Practical rule

Default → Server Component Need hooks/events → 'use client' at the leaf Pass data to client → plain objects, IDs, children Mutations → Server Actions Live client-only data → client fetch or polling

Last updated on