Fetching in Server Components
In the App Router, Server Components can be async functions. That single capability changes how data fetching works in a React app: instead of rendering a component, then firing an effect, then waiting for a loading state, then re-rendering with data, a Server Component can simply await its data before it ever renders anything to the client.
An async component is the fetch
Mark the component function async and await fetch (or a database call) directly inside it. There is no useEffect, no useState for loading/error/data, and no extra render pass — the component only renders once it actually has data.
// app/[locale]/dashboard/page.tsx
interface Order {
id: string
total: number
status: string
}
async function getOrders(): Promise<Order[]> {
const res = await fetch('https://api.example.com/orders', {
next: { revalidate: 60 },
})
if (!res.ok) {
throw new Error('Failed to fetch orders')
}
return res.json()
}
export default async function DashboardPage() {
const orders = await getOrders()
return (
<ul>
{orders.map((order) => (
<li key={order.id}>
Order #{order.id} — {order.status} — ${order.total}
</li>
))}
</ul>
)
}This runs entirely on the server. The fetch call, the parsing, and the rendering into HTML all happen before anything is sent to the browser — the client receives finished markup, not a spinner followed by an API call.
No loading state needed for the initial render
Because the component does not render until its data is ready, there is nothing to show conditionally while waiting. The classic pattern of tracking isLoading, error, and data in useState — and rendering a spinner while isLoading is true — simply does not apply to the initial render of a Server Component.
// app/[locale]/dashboard/loading.tsx
export default function Loading() {
// Shown automatically while DashboardPage's data is being fetched
return <p>Loading dashboard…</p>
}Direct database access without an API layer
Because Server Components run only on the server and their code is never sent to the browser, they can talk directly to a database, an ORM, or a file system — no need to build a separate REST or GraphQL endpoint just to expose data that only your own app consumes.
// app/[locale]/products/page.tsx
import { db } from '@libs/db'
export default async function ProductsPage() {
// Runs directly on the server, no API route required
const products = await db.product.findMany({
orderBy: { createdAt: 'desc' },
})
return (
<ul>
{products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
)
}Fetching at multiple levels of the tree
Layouts, pages, and any Server Component nested inside them can each independently fetch their own data. This keeps data requirements close to where they are used, instead of one top-level component fetching everything and passing it down through many layers of props.
A layout can fetch data shared across every page it wraps, such as navigation or account info.
A page can fetch the data specific to that route.
A deeply nested Server Component can fetch its own data without needing it threaded through parent props.
Handling errors
A thrown error inside an async Server Component is caught by the nearest error.tsx boundary in the route segment, which lets you show a friendly fallback UI instead of a blank page or an unhandled exception.
// app/[locale]/dashboard/error.tsx
'use client'
export default function Error({ error }: { error: Error }) {
return <p>Something went wrong loading the dashboard: {error.message}</p>
}