Data fetching and streaming
Data directly in Server Components, in parallel, Suspense and params as a Promise.
Updated
What data fetching means in Next
Data fetching = getting the data (from a DB, from an API) a page needs. In the Next App Router, the default place is the Server Component: an async function that reads the data directly and renders the result.
export default async function Page() {
const posts = await db.post.findMany() // an ORM, SQL, fetch — anything
return <PostList posts={posts} />
}Compared with the classic React way (useEffect + fetch + useState): no loading state on the client, no intermediate API route, no exposed secrets, no client → server → DB waterfall.
Ways to get data — and when
| Way | Where | When |
|---|---|---|
a direct await in a Server Component |
the server | the default — any data display |
a Promise passed to a Client Component + use() |
the server starts it, the client reads it | the client component needs data, but you want streaming |
| TanStack Query | the client | very interactive data (polling, infinite scroll) — Server state |
| a Route Handler | the server | an API for others / webhooks — not for your own pages |
useEffect + fetch |
the client | almost never |
Parallel, not a cascade
A waterfall = requests that wait on each other for no reason.
// ✗ a waterfall — 2 × the time
const user = await getUser(id)
const posts = await getPosts(id)
// ✓ parallel
const [user, posts] = await Promise.all([getUser(id), getPosts(id)])What streaming is
Without streaming, the user sees the page only when all the data is ready. With streaming, the server sends the HTML in pieces: what's ready shows immediately, the slow parts arrive when they finish.
The tool: <Suspense>.
export default function Page() {
return (
<>
<Header /> {/* shows immediately */}
<Suspense fallback={<Skeleton />}>
<Recommendations /> {/* async, shows when it's ready */}
</Suspense>
</>
)
}loading.tsx = an automatic <Suspense> around the whole page.
A Promise passed to the client
You start the request on the server, but read it in a Client Component:
// server
const statsPromise = getStats() // no await — doesn't block
<Suspense fallback={<Spinner />}>
<Stats data={statsPromise} />
</Suspense>
// client
'use client'
import { use } from 'react'
export function Stats({ data }: { data: Promise<Stats> }) {
const stats = use(data) // suspends until it arrives
}Runtime data
cookies(), headers(), searchParams, params (for values that weren't pre-generated) depend on the request — they can't be known at build time. The components that read them become dynamic and must be wrapped in <Suspense>, so the rest of the page stays static. That's exactly how your progress is shown in this app.
Deduplication
The same function called from the layout + the page + generateMetadata?
| Tool | Lifetime |
|---|---|
React's cache() |
for the duration of one request |
'use cache' |
persistent, across requests — the Caching lesson |
Summary
- Data → directly in Server Components, with
await. Promise.allfor independent requests.<Suspense>for streaming; runtime data goes inside Suspense.