Server Components Fetching
Server Components fundamentally change how we fetch data. No more useEffect, no more loading states in components, no more API routes for internal data.
The Simple Pattern
Server Components can be async. Just fetch directly:
// app/products/page.tsx
async function ProductsPage() {
const products = await fetch('https://api.example.com/products')
.then(res => res.json())
return (
<ul>
{products.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
)
}
export default ProductsPage
No useState, no useEffect, no loading state management. The page renders with data.
Direct Database Access
Skip the API entirely for internal data:
import { db } from '@/lib/db'
async function Dashboard() {
// Direct database query - no API needed
const stats = await db.query(`
SELECT COUNT(*) as users FROM users
`)
const recentOrders = await db.query(`
SELECT * FROM orders
ORDER BY created_at DESC
LIMIT 5
`)
return (
<div>
<h1>Users: {stats.users}</h1>
<OrderList orders={recentOrders} />
</div>
)
}
Parallel Data Fetching
Fetch multiple things at once:
async function Dashboard() {
// ❌ Sequential - slow
const user = await getUser()
const posts = await getPosts()
const comments = await getComments()
// ✅ Parallel - fast
const [user, posts, comments] = await Promise.all([
getUser(),
getPosts(),
getComments()
])
return <DashboardView user={user} posts={posts} comments={comments} />
}
Streaming with Suspense
For slow data, stream it in progressively:
import { Suspense } from 'react'
async function SlowRecommendations() {
const recs = await getRecommendations() // Takes 3 seconds
return <RecommendationList items={recs} />
}
export default function ProductPage() {
return (
<div>
<ProductDetails /> {/* Shows immediately */}
<Suspense fallback={<RecommendationsSkeleton />}>
<SlowRecommendations /> {/* Streams in when ready */}
</Suspense>
</div>
)
}
The page doesn't wait for slow data. Fast content shows immediately.
Fetch Memoization
Next.js automatically deduplicates fetch requests:
// These are the SAME request - only fetched once
async function Header() {
const user = await fetch('/api/user').then(r => r.json())
return <nav>{user.name}</nav>
}
async function Sidebar() {
const user = await fetch('/api/user').then(r => r.json())
return <aside>{user.avatar}</aside>
}
// Both components get the same cached result
This works automatically for the native fetch API during a single render.
Error Handling
Use error boundaries with the error.tsx convention:
// app/dashboard/error.tsx
'use client'
export default function Error({
error,
reset,
}: {
error: Error
reset: () => void
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
)
}
Or handle errors in the component:
async function UserProfile({ userId }) {
const user = await getUser(userId)
if (!user) {
notFound() // Shows not-found.tsx
}
return <Profile user={user} />
}
Summary
- Server Components can be async - fetch data directly
- Access databases without API routes for internal data
- Use
Promise.all()for parallel fetching - Wrap slow data in Suspense for streaming
- Fetch requests are automatically deduplicated
- Use
error.tsxandnot-found.tsxfor error states

