Skip to main content
Chapter 3 Data Fetching and Server Components

Async Components and the Fetch API

20 min read Lesson 10 / 28

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.