billing hacks

Server vs Client Components

The App Router changed everything. Here's what actually happens and how to avoid common mistakes.

TL;DR

Server Components are the default - they run on server only. 'use client' doesn't mean client-only, it means "needs hydration." Keep client boundaries small. Use the children pattern for wrappers.

The Two Types

// No directive = Server Component (default)

export default async function ProductList() {
  const products = await db.products.findMany();
  return <ul>{products.map(p => <li>{p.name}</li>)}</ul>;
}
  • Runs on server only
  • Can use async/await directly
  • Can access DB, filesystem, secrets
  • Cannot use useState, useEffect, onClick
'use client';

export default function AddToCart({ productId }) {
  const [loading, setLoading] = useState(false);

  return (
    <button onClick={() => setLoading(true)}>
      Add to Cart
    </button>
  );
}
  • Renders on server first (SSR), hydrates on client
  • Can use all React hooks
  • Can use browser APIs
  • Cannot be async

'use client' doesn't mean "client-only". It means "this component needs JavaScript to be interactive." It still SSRs.

The Boundary Rule

Client components cannot import Server components:

'use client';

import ServerThing from './ServerThing'; // ❌ Breaks

But Server components can render Client components:

// Server Component
import ClientButton from './ClientButton';

export default async function Page() {
  const data = await getData();
  return (
    <div>
      <h1>{data.title}</h1>
      <ClientButton /> {/* ✅ Works */}
    </div>
  );
}

The Children Pattern

Need a Client wrapper around Server content? Use children:

'use client';

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext.Provider value={theme}>
      {children}
    </ThemeContext.Provider>
  );
}
page.tsx
export default function Page() {
  return (
    <ThemeProvider>
      <ServerContent /> {/* ✅ Works via children */}
    </ThemeProvider>
  );
}

Common Mistakes

Mistake 1: Browser APIs Without Checks

'use client';

// ❌ Breaks during SSR
const width = window.innerWidth;

// ✅ Check environment
const width = typeof window !== 'undefined' ? window.innerWidth : 0;

// ✅ Or use useEffect
const [width, setWidth] = useState(0);
useEffect(() => setWidth(window.innerWidth), []);

Mistake 2: Browser-Only Libraries

'use client';

// ❌ Library accesses window on import
import Confetti from 'react-confetti';

// ✅ Dynamic import with ssr: false
import dynamic from 'next/dynamic';
const Confetti = dynamic(() => import('react-confetti'), { ssr: false });

Mistake 3: Hydration Mismatch

'use client';

// ❌ Server and client render different values
const [time, setTime] = useState(new Date().toISOString());

// ✅ Initialize empty, set on client
const [time, setTime] = useState<string | null>(null);
useEffect(() => setTime(new Date().toISOString()), []);

Mistake 4: Unnecessary Client Components

// ❌ No interactivity - doesn't need 'use client'
'use client';

export default function ProductCard({ product }) {
  return (
    <div>
      <h2>{product.name}</h2>
      <p>{product.price}</p>
    </div>
  );
}

// ✅ Remove the directive - let it be a Server Component
export default function ProductCard({ product }) {
  return (
    <div>
      <h2>{product.name}</h2>
      <p>{product.price}</p>
    </div>
  );
}

Only add 'use client' when you need hooks or event handlers.

Decision Guide

Your Component NeedsUse
Database/API callsServer
useState, useEffectClient
onClick, onChangeClient
Just displays propsServer
Uses window/documentClient
Reads cookies/headersServer

The Composition Pattern

Keep client boundaries small:

// ❌ Entire page is client
'use client';

export default function ProductPage() {
  const [qty, setQty] = useState(1);
  // ... lots of static content ...
  return <div>...</div>;
}

// ✅ Only interactive part is client
// ProductPage.tsx (Server Component)
export default async function ProductPage() {
  const product = await getProduct();
  return (
    <div>
      <h1>{product.name}</h1>        {/* Server */}
      <p>{product.description}</p>   {/* Server */}
      <QuantityPicker />             {/* Client */}
    </div>
  );
}

// QuantityPicker.tsx
'use client';
export function QuantityPicker() {
  const [qty, setQty] = useState(1);
  return <input value={qty} onChange={e => setQty(e.target.value)} />;
}

Less client JavaScript = faster page loads.

Key Takeaways

Audit 'use client' usage Only components with hooks or event handlers need this directive.

Protect browser APIs Wrap window/document in typeof window !== 'undefined' or useEffect.

Handle browser-only libraries Use dynamic(() => import(...), { ssr: false }) for libraries that access browser APIs on import.

Fix hydration mismatches Check console for warnings. Initialize client-dependent state in useEffect.

Keep client boundaries small Extract only the interactive parts into Client components.


With fundamentals covered, see Caching Strategies for Vercel-specific optimizations.

On this page