Server Components vs Client Components
In Next.js, every component is a Server Component by default. This means it runs on the server, can access databases and APIs directly, and sends only the rendered HTML to the browser.
Client Components run in the browser and are needed when you use interactivity — state, effects, event handlers, or browser APIs.
// This is a Server Component (default)
// It runs on the server — no JavaScript sent to the browser
export default async function ProductList() {
const products = await fetch('https://api.example.com/products')
.then(res => res.json())
return (
<ul>
{products.map(p => (
<li key={p.id}>{p.name} — ${p.price}</li>
))}
</ul>
)
}To make a component a Client Component, add the "use client" directive at the top:
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<button onClick={()=> setCount(count + 1)}>
Clicks: {count}
</button>
)
}When to Use Each
| Use Server Components when... | Use Client Components when... |
|---|---|
| Fetching data | Handling user events (click, input) |
| Accessing backend resources | Using React state or effects |
| Keeping sensitive data on the server | Using browser-only APIs |
| Reducing client JavaScript | Adding interactivity |
A good rule: keep components on the server by default and only add "use client" when you need interactivity.
Fetching Data in Server Components
Server Components can use async/await directly — no useEffect or useState needed:
// app/users/page.tsx
interface User {
id: number
name: string
email: string
}
export default async function UsersPage() {
const res = await fetch('https://jsonplaceholder.typicode.com/users')
const users: User[] = await res.json()
return (
<div>
<h1>Users</h1>
{users.map(user => (
<div key={user.id}>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
))}
</div>
)
}This is simpler than the traditional React approach — no loading states to manage, no useEffect timing issues.
Caching and Revalidation
Next.js extends fetch with caching options:
// Cache the response indefinitely (default behavior)
const res = await fetch('https://api.example.com/data')
// Revalidate every 60 seconds
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 60 }
})
// Never cache — always fetch fresh data
const res = await fetch('https://api.example.com/data', {
cache: 'no-store'
})- Static (default) — data is fetched at build time and cached
- Revalidate — data is cached but refreshed after a time interval
- Dynamic — data is fetched on every request
Loading and Error States
Next.js has special files for handling loading and errors at the route level:
// app/users/loading.tsx
export default function Loading() {
return <p>Loading users...</p>
}// app/users/error.tsx
'use client'
export default function Error({ error, reset }) {
return (
<div>
<p>Something went wrong: {error.message}</p>
<button onClick={()=> reset()}>Try Again</button>
</div>
)
}The loading.tsx file shows automatically while the page's data is being fetched. The error.tsx file catches errors and gives users a way to recover.
Composing Server and Client Components
A common pattern is wrapping interactive Client Components inside data-fetching Server Components:
// app/dashboard/page.tsx (Server Component)
import Chart from './chart'
export default async function Dashboard() {
const data = await fetch('https://api.example.com/stats')
.then(res => res.json())
// Pass server-fetched data to an interactive client component
return <Chart data={data} />
}// app/dashboard/chart.tsx (Client Component)
'use client'
export default function Chart({ data }) {
// Interactive chart using browser APIs
return <canvas id="chart">{/* render chart with data */}</canvas>
}This keeps your data fetching on the server while letting the chart handle mouse events and animations in the browser.
Practical Exercise
Try building a simple page that fetches data:
- Create
app/posts/page.tsxas a Server Component - Fetch posts from
https://jsonplaceholder.typicode.com/posts?_limit=10 - Render each post's title and body
- Add a
loading.tsxfile in the same folder - Test it by visiting
/posts