principlejavascriptreactMajor
Server Components vs Client Components — the mental model
Viewed 0 times
React 18+ with supported frameworks (Next.js 13+)
server componentsclient componentsuse clientuse serverApp RouterNext.jsislandsbundle size
browsernodejs
Error Messages
Problem
Developers treat the Server/Client split as arbitrary — adding 'use client' everywhere to silence errors, or putting data fetching in Client Components when it should stay on the server. This leads to large client bundles, unnecessary waterfalls, and security issues (leaking secrets to the browser).
Solution
Follow the rule: start on the server, push interactivity to the edge of the tree.
// Server Component (default in Next.js App Router)
// Can: async/await, DB access, use env secrets, reduce bundle
// Cannot: useState, useEffect, onClick, browser APIs
export default async function ProductList() {
const products = await db.query('SELECT * FROM products'); // runs on server
return <ul>{products.map(p => <ProductCard key={p.id} product={p} />)}</ul>;
}
// Client Component — only for interactivity
'use client';
function AddToCartButton({ productId }) {
const [added, setAdded] = useState(false);
return (
<button onClick={() => { addToCart(productId); setAdded(true); }}>
{added ? 'Added!' : 'Add to Cart'}
</button>
);
}
// Composition pattern — Server renders, Client handles interaction
// ProductCard.tsx (Server Component)
export function ProductCard({ product }) {
return (
<li>
<h2>{product.name}</h2>
<AddToCartButton productId={product.id} /> {/ Client island /}
</li>
);
}
// Server Component (default in Next.js App Router)
// Can: async/await, DB access, use env secrets, reduce bundle
// Cannot: useState, useEffect, onClick, browser APIs
export default async function ProductList() {
const products = await db.query('SELECT * FROM products'); // runs on server
return <ul>{products.map(p => <ProductCard key={p.id} product={p} />)}</ul>;
}
// Client Component — only for interactivity
'use client';
function AddToCartButton({ productId }) {
const [added, setAdded] = useState(false);
return (
<button onClick={() => { addToCart(productId); setAdded(true); }}>
{added ? 'Added!' : 'Add to Cart'}
</button>
);
}
// Composition pattern — Server renders, Client handles interaction
// ProductCard.tsx (Server Component)
export function ProductCard({ product }) {
return (
<li>
<h2>{product.name}</h2>
<AddToCartButton productId={product.id} /> {/ Client island /}
</li>
);
}
Why
Server Components run only on the server — their code never reaches the browser. This means secrets stay safe, database drivers don't ship to the client, and the component itself doesn't add to bundle size. Client Components form 'islands of interactivity' in an otherwise server-rendered tree.
Gotchas
- A Client Component can receive Server Components as children (props.children) — but cannot import them
- Server Components cannot use context — pass data as props or use a Client Component as the context provider
- Serializable props only: you cannot pass functions, class instances, or Dates from Server to Client Components
- 'use client' marks a boundary — all imports below that boundary also become client code
Code Snippets
Server vs Client decision guide
// Decision tree:
// Does it need useState/useEffect/onClick/browser APIs? → 'use client'
// Does it fetch data or access backend? → Server Component
// Does it do both? → Split into two componentsContext
When building with Next.js App Router or any React framework using Server Components
Revisions (0)
No revisions yet.