Fetching Data in Server Components
IntermediateAn 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.
// 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.
// 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 flightRequest Memoization
Why calling the same fetch in five components is fine.
// 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 AriaHow does data fetching in a server component differ from useEffect?
What is a request waterfall in the App Router and how do you avoid one?
Why can you call the same data function in a layout and a page without fetching twice?
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.