Fetching Data Directly in Components
The App Router lets any Server Component be async. This means you write data fetching as plain await calls inside your component — no useEffect, no loading states, no API layers needed for simple cases.
Basic Async Server Component
// app/blog/page.tsx
interface Post {
id: string;
title: string;
slug: string;
excerpt: string;
publishedAt: string;
}
async function getPosts(): Promise<Post[]> {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 }, // Cache for 1 hour
});
if (!res.ok) {
throw new Error('Failed to fetch posts');
}
return res.json();
}
export default async function BlogPage() {
const posts = await getPosts();
return (
<div className="max-w-4xl mx-auto py-12">
<h1 className="text-3xl font-bold mb-8">Blog</h1>
<div className="space-y-6">
{posts.map(post => (
<article key={post.id} className="border-b pb-6">
<h2 className="text-xl font-semibold">{post.title}</h2>
<p className="text-gray-600 mt-2">{post.excerpt}</p>
</article>
))}
</div>
</div>
);
}
Parallel Data Fetching
Avoid waterfalls by fetching independently needed data in parallel:
// Bad — sequential waterfall (slow)
async function DashboardPage() {
const user = await getUser(); // Wait...
const posts = await getPosts(); // Then wait...
const analytics = await getAnalytics(); // Then wait...
// Total: sum of all three requests
}
// Good — parallel fetching (fast)
async function DashboardPage() {
const [user, posts, analytics] = await Promise.all([
getUser(),
getPosts(),
getAnalytics(),
]);
// Total: duration of the longest single request
}
Streaming with Suspense
Break a slow page into independently streamed sections:
import { Suspense } from 'react';
export default function Dashboard() {
return (
<div className="grid grid-cols-3 gap-6">
{/* Renders immediately */}
<WelcomeCard />
{/* Streams in when ready */}
<Suspense fallback={<StatsSkeleton />}>
<StatsSection />
</Suspense>
<Suspense fallback={<FeedSkeleton />}>
<RecentActivity />
</Suspense>
</div>
);
}
async function StatsSection() {
const stats = await getStats(); // Can take 500ms
return <StatsGrid stats={stats} />;
}
The browser receives the page shell instantly and each section streams in as its data resolves.