NextjsFetching in Server Components

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.

TSX
// 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.

Note
You can still show a loading UI for the route while the Server Component is fetching, using a loading.tsx file (backed by React Suspense) alongside page.tsx. That gives you an instant loading state at the navigation level without any component-local state at all.

TSX
// 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.

TSX
// 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>
  )
}
Warning
Direct database access only belongs in Server Components (or Server Actions / Route Handlers). Never import a database client into a file marked 'use client' — secrets and connection logic would end up in the browser bundle.
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.

Tip
If the same URL is fetched from more than one component during a single render, Next.js and React automatically de-duplicate those calls via Request Memoization — see the dedicated page in this section for details.
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.

TSX
// 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>
}