2. Server vs Client Components - A Detailed Guide
A document explaining the two component types in the App Router, when to use each, and how Nova Shop splits products/page.tsx (server) vs productForm (client).
On this page
- Table of Contents
- 1. The Problem: SPA vs RSC
- 2. The Two Component Types - Comparison Table
- 3. How Does a Server Component Actually Run?
- 4. Client Components - When They're Required
- 5. Composition: Server Parent, Client Leaves
- 6. Passing Props & Server Actions
- 7. Suspense, Streaming, loading.tsx
- 8. Nova Shop - the Real File Map
- 9. FAQ & Common Mistakes
- 10. File Map
2. Server vs Client Components - A Detailed Guide
A document explaining the two component types in the App Router, when to use each, and how Nova Shop splits products/page.tsx (server) vs productForm (client).
Read in order: Section 1 → 2 (comparison table) → 3–5 (composition, props) → 6 (Nova) → 7 (FAQ).
Official docs: Server and Client Components
Table of Contents
- The problem: SPA vs RSC
- The two component types - comparison table
- How does a Server Component actually run?
- Client Components - when they're required
- Composition: server parent, client leaves
- Passing props & Server Actions
- Suspense, streaming,
loading.tsx - Nova Shop - the real file map
- FAQ & common mistakes
- File map
1. The Problem: SPA vs RSC
A pure React SPA:
Browser downloads a big bundle → useEffect fetches an API → renders
- Secrets/API keys are easily exposed when fetched from the client.
- SEO: crawlers see a skeleton before JS runs.
- Waterfall: layout fetch → child fetch → slow.
Next.js Server Components (RSC):
Server fetches + renders HTML/RSC payload → browser hydrates only the small client part
→ Nova Shop defaults to server-side fetching (getProducts in page.tsx).
2. The Two Component Types - Comparison Table
| Server (default) | Client ("use client") | |
|---|---|---|
| Declaration | No directive needed | "use client" at the top of the file |
| Runs on | Server only | Server (initial SSR) + browser |
| JS sent to client | No (mostly) | Yes - bundle size grows |
useState, useEffect | ❌ | ✅ |
onClick, onChange | ❌ | ✅ |
async function Page() | ✅ | ❌ |
Reading secret process.env | ✅ | ❌ (only NEXT_PUBLIC_) |
| Importing a Client component | ✅ | ✅ |
| Importing a Server component into a Client one | ❌ | ❌ |
Rule of thumb: files inside app/ are Server by default unless they contain "use client".
3. How Does a Server Component Actually Run?
- A Server Component does not become a React bundle that re-runs in the browser.
- Its output is an RSC payload - the client understands the structure + serialized data.
- Client Components are referenced and hydrated (events attached).
Nova example:
// app/(shop)/products/page.tsx - SERVER
export default async function ProductsPage(props) {
const result = await getProducts({ ...filters }); // runs on the server
return (
<>
<ListProductsComponent products={result.products} />
<Suspense fallback={null}>
<ProductToolbar /> {/* CLIENT */}
</Suspense>
</>
);
}
4. Client Components - When They're Required
| Needed for | Nova Shop file |
|---|---|
useState / useTransition | cart-view.tsx, productForm.tsx |
useRouter, useSearchParams | product-toolbar.tsx, navbar.tsx |
onClick, form events | BuyNowButton.tsx, login-form.tsx |
localStorage, window | cart-events.ts, navbar.tsx |
useSession (NextAuth) | navbar.tsx |
| A library that only supports the client | Some charts/maps |
Anti-pattern: putting "use client" on page.tsx just because of a single button → the entire page becomes client-side, losing all the RSC benefits.
5. Composition: Server Parent, Client Leaves
Recommended pattern (per the docs)
Page (Server) - fetches data
├── List (Server or Server) - displays it
└── Toolbar (Client) - handles URL/filter interaction
Advanced pattern: client wrapper, server children
"use client";
export function ClientShell({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(false);
return <div>{children}</div>; // children can still be Server Components
}
// page.tsx - server
<ClientShell>
<ServerProductList /> {/* still server */}
</ClientShell>
Note: don't import a Server Component directly into a client file - only pass it through the children slot.
6. Passing Props & Server Actions
Server → Client props
Must be serializable (JSON-safe):
// ✅
<BuyNowButton product={{ id: "1", name: "Phone", price: 999 }} />
// ❌ functions, class instances, and Dates don't serialize
<Child onLoad={() => fetch()} />
Server Actions - the exception
The client can do import { addToCart } from "@/app/lib/services/cart" - Next.js creates an internal POST endpoint for it. See 3. Server Actions.
7. Suspense, Streaming, loading.tsx
<Suspense fallback={<Skeleton />}>
<ProductToolbar /> {/* client, uses useSearchParams */}
</Suspense>
| Mechanism | Effect |
|---|---|
<Suspense> | Streams the slow part after the fast part |
loading.tsx | An automatic Suspense boundary for a segment |
Nova: ProductToolbar + ProductPagination are wrapped in Suspense because useSearchParams requires a boundary (a Next.js requirement).
8. Nova Shop - the Real File Map
| File | Type | Reason |
|---|---|---|
(shop)/products/page.tsx | Server | async + getProducts |
listProductsComponent.tsx | Server* | Only renders props, no hooks |
product-toolbar.tsx | Client | useRouter, changes URL filters |
productForm.tsx | Client | addToCart, quantity state |
navbar.tsx | Client | useSession, scroll, cart badge |
providers.tsx | Client | SessionProvider |
shop-shell.tsx | Server | Wraps Suspense + the client navbar |
*If a hook is added later → must be converted to client.
An Area for Improvement (vs. Best Practice)
navbar.tsx calls getCart() and getUser() from the client → adds an extra round trip.
A better approach:
// (shop)/layout.tsx - server async
const cartCount = (await getCartSummary()).totalItems;
return <ShopShell cartCount={cartCount}>{children}</ShopShell>;
9. FAQ & Common Mistakes
FAQ
Q: Does a Server Component re-run on every request?
On a dynamic route (cookies, no-store fetch) - yes, it renders fresh on every request.
Q: Does "use client" at the top of a child file make the parent client too?
No - only that file and the import tree below it inside that file.
Q: What about the Context API?
The Provider must be on the client (SessionProvider). Server children wrapped by a provider are still fine.
Common Mistakes
| Mistake | Cause | Fix |
|---|---|---|
"needs useState" error | A hook inside a server file | Split out a client file |
| Hydration mismatch | Date.now() / a random id during render | Only randomize on the client, inside useEffect |
| A leaked secret | NEXT_PUBLIC_STRIPE_SECRET | Drop the PUBLIC prefix |
10. File Map
app/(shop)/products/page.tsx → Server, entry point
app/ui/products/product-toolbar.tsx → Client
app/(dashboard)/products/[slug]/productForm.tsx → Client (if that path still exists)
app/providers.tsx → Client, root
Next: 3. Server Actions · 5. Data Fetching
