Intermediate18 min1 prerequisite

Understand the critical distinction between Server and Client Components in Next.js and when to use each.

Server vs Client Components

The Server Component and Client Component distinction is the most important concept to understand when working with AI-generated Next.js code.

The Fundamental Difference

Server Components (Default)

Run only on the server:

Terminal
// app/products/page.tsx
// Server Component by default - no directive needed

export default async function ProductsPage() {
  // This code runs on the server
  const products = await db.product.findMany()

  // Data is already loaded when HTML is sent to browser
  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  )
}

Characteristics:

  • Run at build time or request time
  • Can access databases directly
  • Can use server-only code
  • No JavaScript sent to browser
  • Cannot use hooks (useState, useEffect)
  • Cannot handle events (onClick, onChange)

Client Components

Run in the browser:

Terminal
// components/Counter.tsx
'use client' // This directive marks it as a Client Component

import { useState } from 'react'

export default function Counter() {
  // This code runs in the browser
  const [count, setCount] = useState(0)

  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  )
}

Characteristics:

  • JavaScript sent to browser
  • Can use React hooks
  • Can handle user events
  • Cannot be async
  • No direct database access

When to Use Each

Use Server Components For:

Terminal
// Fetching data
async function ProductPage({ params }) {
  const product = await db.product.findUnique({
    where: { id: params.id }
  })
  return <ProductDetails product={product} />
}

// Accessing backend resources
async function Dashboard() {
  const analytics = await getAnalytics()
  const reports = await generateReports()
  return <DashboardView data={{ analytics, reports }} />
}

// Rendering static content
function TermsOfService() {
  return (
    <article>
      <h1>Terms of Service</h1>
      <p>Last updated: ...</p>
      {/* Long content */}
    </article>
  )
}

Use Client Components For:

Terminal
'use client'

// Interactivity
function LikeButton({ postId }) {
  const [liked, setLiked] = useState(false)

  return (
    <button onClick={() => setLiked(!liked)}>
      {liked ? '❀️' : '🀍'}
    </button>
  )
}

// Browser APIs
function LocationTracker() {
  const [location, setLocation] = useState(null)

  useEffect(() => {
    navigator.geolocation.getCurrentPosition(pos => {
      setLocation(pos.coords)
    })
  }, [])

  return <div>Lat: {location?.latitude}</div>
}

// Real-time updates
function LiveChat() {
  const [messages, setMessages] = useState([])

  useEffect(() => {
    const ws = new WebSocket('ws://...')
    ws.onmessage = e => setMessages(prev => [...prev, e.data])
    return () => ws.close()
  }, [])

  return <MessageList messages={messages} />
}

Decision Flowchart

Terminal
Do you need...

useState, useEffect, hooks?
β”œβ”€β”€ Yes β†’ Client Component ('use client')
└── No ↓

onClick, onChange, events?
β”œβ”€β”€ Yes β†’ Client Component ('use client')
└── No ↓

Browser APIs (window, localStorage, etc)?
β”œβ”€β”€ Yes β†’ Client Component ('use client')
└── No ↓

Direct database access?
β”œβ”€β”€ Yes β†’ Server Component (default)
└── No ↓

Use Server Component (default)

Composing Server and Client Components

Pattern 1: Server Parent, Client Child

The most common pattern:

Terminal
// app/products/[id]/page.tsx (Server)
import { AddToCartButton } from './AddToCartButton'

export default async function ProductPage({ params }) {
  const product = await getProduct(params.id)

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>

      {/* Client Component receives server-fetched data */}
      <AddToCartButton productId={product.id} />
    </div>
  )
}
Terminal
// app/products/[id]/AddToCartButton.tsx
'use client'

import { useState } from 'react'

export function AddToCartButton({ productId }: { productId: string }) {
  const [adding, setAdding] = useState(false)

  async function handleAdd() {
    setAdding(true)
    await addToCart(productId)
    setAdding(false)
  }

  return (
    <button onClick={handleAdd} disabled={adding}>
      {adding ? 'Adding...' : 'Add to Cart'}
    </button>
  )
}

Pattern 2: Pass Server Components as Children

Terminal
// components/ClientWrapper.tsx
'use client'

import { useState } from 'react'

export function Accordion({ title, children }) {
  const [isOpen, setIsOpen] = useState(false)

  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>{title}</button>
      {isOpen && children}
    </div>
  )
}
Terminal
// app/faq/page.tsx (Server)
import { Accordion } from '@/components/ClientWrapper'

export default async function FAQPage() {
  const faqs = await getFAQs()

  return (
    <div>
      {faqs.map(faq => (
        <Accordion key={faq.id} title={faq.question}>
          {/* This content is Server-rendered, passed as children */}
          <div dangerouslySetInnerHTML={{ __html: faq.answer }} />
        </Accordion>
      ))}
    </div>
  )
}

Pattern 3: Lifting Data Fetching

Move data fetching up:

Terminal
// ❌ Bad: Fetching in Client Component
'use client'
function ProductList() {
  const [products, setProducts] = useState([])

  useEffect(() => {
    fetch('/api/products')
      .then(res => res.json())
      .then(setProducts)
  }, [])

  return products.map(p => <Product key={p.id} {...p} />)
}

// βœ… Good: Fetch in Server, pass to Client
// app/products/page.tsx (Server)
async function ProductsPage() {
  const products = await getProducts()

  return <ProductList products={products} />
}

// components/ProductList.tsx (Client)
'use client'
function ProductList({ products }) {
  const [filter, setFilter] = useState('')

  const filtered = products.filter(p =>
    p.name.includes(filter)
  )

  return (
    <>
      <input onChange={e => setFilter(e.target.value)} />
      {filtered.map(p => <Product key={p.id} {...p} />)}
    </>
  )
}

Props and Boundaries

Serializable Props Only

Props passed from Server to Client must be serializable:

Terminal
// βœ… Works - primitive values and plain objects
<ClientComponent
  name="John"
  count={42}
  user={{ id: 1, name: 'John' }}
  items={['a', 'b', 'c']}
/>

// ❌ Doesn't work - functions, classes, symbols
<ClientComponent
  onClick={() => {}}           // Function
  date={new Date()}            // Date object
  user={userInstance}          // Class instance
/>

The Boundary

Terminal
// Client boundary is established by 'use client'
'use client'

// Everything below is client-side
import { useState } from 'react'
import { HelperComponent } from './Helper' // Also becomes client

export function ClientComponent() {
  // ...
}

Key insight: When you import a component into a Client Component, it also becomes a Client Component (unless passed as children).

Common Mistakes

Mistake 1: Using Hooks in Server Components

Terminal
// ❌ Error: useState not allowed in Server Components
export default function ProductPage() {
  const [loading, setLoading] = useState(false)
  // ...
}

// βœ… Move state to a Client Component
export default async function ProductPage() {
  const product = await getProduct()
  return <ProductView product={product} />
}

// components/ProductView.tsx
'use client'
export function ProductView({ product }) {
  const [quantity, setQuantity] = useState(1)
  // ...
}

Mistake 2: Async Client Components

Terminal
// ❌ Client Components cannot be async
'use client'
export default async function Profile() {
  const user = await getUser()
  // ...
}

// βœ… Fetch in Server Component, pass as props
// Server Component
export default async function ProfilePage() {
  const user = await getUser()
  return <Profile user={user} />
}

// Client Component
'use client'
export function Profile({ user }) {
  // Has data from server
}

Mistake 3: Browser APIs in Server Components

Terminal
// ❌ window doesn't exist on server
export default function Nav() {
  const path = window.location.pathname
  // ...
}

// βœ… Use as Client Component
'use client'
import { usePathname } from 'next/navigation'

export function Nav() {
  const path = usePathname()
  // ...
}

Mistake 4: Database in Client Components

Terminal
// ❌ Can't access DB from client
'use client'
export function Dashboard() {
  const data = await prisma.user.findMany() // Won't work
  // ...
}

// βœ… Use Server Actions or API routes
'use client'
import { getUsers } from './actions'

export function Dashboard() {
  const [users, setUsers] = useState([])

  useEffect(() => {
    getUsers().then(setUsers)
  }, [])
}

AI-Generated Code Patterns

Recognizing Server Components

Terminal
// Server Component indicators:
// - No 'use client' at top
// - async function
// - Direct data fetching
// - No hooks or event handlers

export default async function Page() {
  const data = await fetchData()
  return <View data={data} />
}

Recognizing Client Components

Terminal
// Client Component indicators:
// - 'use client' at top
// - useState, useEffect, etc.
// - onClick, onChange, etc.
// - Browser API usage

'use client'

import { useState } from 'react'

export default function Form() {
  const [value, setValue] = useState('')
  return <input onChange={e => setValue(e.target.value)} />
}

When AI Gets It Wrong

Sometimes AI generates code that needs adjustment:

Terminal
// AI might generate this (wrong):
'use client'

export default async function ProductsPage() {
  const products = await db.product.findMany()
  return <ProductList products={products} />
}

// You should fix to (correct):
// Split into Server and Client
export default async function ProductsPage() {
  const products = await db.product.findMany()
  return <ProductListClient products={products} />
}

// Separate file with 'use client'

Summary

AspectServer ComponentClient Component
DirectiveNone (default)'use client'
Runs whereServerBrowser
Data fetchingDirect (async)Via hooks/actions
Hooks❌ Not allowedβœ… Allowed
Events❌ Not allowedβœ… Allowed
Bundle sizeNo JS sentJS sent

Next Steps

Learn about data fetching patterns and Server Actions in the next lesson.

Mark this lesson as complete to track your progress