Home/Learn/Next.js/Fetching Data in Server Components

Fetching Data in Server Components

Intermediate
Data & Caching

An async component that awaits its own data. No useEffect, no loading flag, no API route in the middle — and the waterfall problem moves from the browser to your control.

Overview

The single biggest simplification in the App Router is that a component can be async and await its data directly. Everything the React track spent three concepts on — the loading flag, the error state, the race condition, the abort — is gone, because there is no client-side request to coordinate. What replaces it is a different discipline: components fetch their own data, which is good for colocation and dangerous for waterfalls, so knowing when to parallelise is the skill that carries over.

Await Directly

The whole pattern, and what it replaces.

async component, await, throw
// app/problems/page.tsx — a server component
export default async function ProblemsPage() {
  const problems = await getProblems()        // no useEffect, no state
  return <ProblemList problems={problems} />
}

// It can talk to the database directly, because it runs on the server
import { db } from '@/lib/db'
const problems = await db.problem.findMany({ where: { published: true } })

// Or to your own API — for this platform, FastAPI on Fly:
const res = await fetch(`${process.env.API_URL}/problems`, {
  headers: { Authorization: `Bearer ${process.env.SERVICE_TOKEN}` },
})
if (!res.ok) throw new Error(`API ${res.status}`)   // -> error.tsx
const problems = await res.json()

// A thrown error goes to the nearest error.tsx. A notFound() goes to
// not-found.tsx. There is no error STATE to manage — the boundaries
// are the error handling.

// Note what is NOT needed here: an /api route in your own app just to
// reach your own database. That indirection exists in the Pages
// Router because the browser was doing the fetching; here it is pure
// overhead.

Waterfalls

The one performance mistake this design makes easy.

Promise.all for independent, Suspense for components
// Sequential — each await waits for the last. 3 x 200ms = 600ms.
const user = await getUser(id)
const orders = await getOrders(user.id)        // genuinely dependent
const recs = await getRecommendations()        // NOT dependent — wasted

// Parallel — start them together, 200ms total
const [user, recs] = await Promise.all([
  getUser(id),
  getRecommendations(),
])
const orders = await getOrders(user.id)        // still dependent, still after

// Component-level waterfalls are subtler. Nested async components
// each await in turn, so a three-deep tree serialises:
<Layout>          await A
  <Section>       await B      // starts only after A resolves
    <Widget>      await C
// Fix: start the fetches in the parent and pass promises down, or
// give each child its own Suspense boundary so they stream in
// parallel (see the streaming concept).

// preload pattern — begin a fetch before you need it
export function preloadProblem(slug: string) { void getProblem(slug) }
preloadProblem(slug)          // kick it off
const user = await getUser()  // do other work meanwhile
const problem = await getProblem(slug)   // already in flight

Request Memoization

Why calling the same fetch in five components is fine.

fetch dedupes; cache() wraps everything else
// React dedupes identical fetch() calls within a single render pass.
// Same URL and options -> one actual request.

// So this is not a bug, and does not need a context or prop drilling:
// layout.tsx
const user = await getCurrentUser()
// page.tsx
const user = await getCurrentUser()      // memoized — no second call
// Header.tsx
const user = await getCurrentUser()      // still one request

// The cache lasts for ONE server render. It is not shared between
// requests or between users — which is what makes it safe.

// It applies to fetch automatically. For anything else — a database
// client, an SDK — wrap it in React's cache():
import { cache } from 'react'
export const getCurrentUser = cache(async () => {
  const sid = cookies().get('sid')?.value
  return sid ? db.session.findUnique({ where: { id: sid } }) : null
})

// This is what makes "fetch where you need it" practical rather than
// wasteful, and it is why a per-request DAL (see the authorization
// concept) does not cost N queries.

Key Points to Remember

  • 1A server component can be async and await its data, removing the loading flag, error state and race handling entirely
  • 2Errors thrown during fetching are caught by error.tsx, so there is no error state to manage
  • 3Independent awaits must be wrapped in Promise.all, or each one waits for the last
  • 4Nested async components serialise; give each a Suspense boundary so they stream in parallel
  • 5React memoizes identical fetch calls within one render, and cache() extends that to database and SDK calls

Interview Questions

Sign in to ask Aria
1

How does data fetching in a server component differ from useEffect?

Medium
2

What is a request waterfall in the App Router and how do you avoid one?

Hard
3

Why can you call the same data function in a layout and a page without fetching twice?

Hard

Ask Aria about Fetching Data in Server Components

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…