Skip to main content

Data Fetching Patterns

In the App Router data is fetched on the server, in the component that needs it: an async Server Component awaits fetch or a database query directly. The same request in several components is deduplicated within one render (fetch automatically, other functions with React cache), so data does not have to be passed down through props. The main risk is a waterfall: start independent requests in parallel and wrap slow parts in <Suspense> so they do not block the page.

Applies to: Next.js 16.4, React 19.3.

Details​

Why on the Server​

  • Direct access to the database and secrets without an extra API.
  • Fewer round trips: the server is close to the data, the client gets the result in one response.
  • Less JavaScript: the fetching code is not sent to the browser.

Patterns​

PatternHowWhen
Sequentialconst user = await getUser(); await getOrders(user.id)The second request needs the result of the first
ParallelStart both, then await Promise.all([a, b])Independent requests
StreamingEach slow component in its own <Suspense>Show the fast parts first
PreloadCall void getItem(id) early, read it later; the function is wrapped in cacheA request can start before a blocking check
Promise to the clientPass an unawaited promise as a prop; read it with use() in a Client ComponentData needed in an interactive component
Deduplicationfetch is memoized; wrap ORM calls in React cacheThe same data in several components
  • Nested async components that each await their own data form a waterfall: the child starts only after the parent has finished.
  • Promise.all waits for the slowest request; with <Suspense> around each part they appear independently.
  • Client-side fetching (SWR, TanStack Query) is still useful for data that changes after the page has loaded: polling, infinite lists.

Example​

Parallel requests; the slow part streams in its own boundary. With Cache Components the segment also needs a loading.tsx, because the page awaits params:

app/artists/[id]/page.tsx
import { Suspense } from 'react';
import { getArtist, getAlbums, getPlaylists } from '@/lib/music';
import { Playlists } from './playlists';

export default async function ArtistPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;

// Both requests start at once
const [artist, albums] = await Promise.all([getArtist(id), getAlbums(id)]);

return (
<>
<h1>{artist.name}</h1>
<p>{albums.length} albums</p>
<Suspense fallback={<p>Loading playlists...</p>}>
<Playlists playlistsPromise={getPlaylists(id)} />
</Suspense>
</>
);
}
app/artists/[id]/playlists.tsx
'use client';

import { use } from 'react';

type Playlist = { id: string; name: string };

export function Playlists({
playlistsPromise,
}: {
playlistsPromise: Promise<Playlist[]>;
}) {
const playlists = use(playlistsPromise); // suspends until resolved

return (
<ul>
{playlists.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}

Follow-up Questions​

  • How do you avoid a waterfall? Start independent requests together, move the awaits down to the components that need the data, and wrap slow parts in <Suspense>.
  • Why not fetch from your own Route Handler in a Server Component? It is an extra HTTP request to the same server; call the data function directly.
  • How is the same request in a layout and a page not made twice? fetch calls with the same URL and options are memoized for one render; for other functions use React cache.
  • What can go wrong with a promise passed to use()? Create it in a Server Component: a promise created while a Client Component renders is new on every render, so use() suspends again. A rejection goes to the nearest error boundary.

Common Mistakes​

  • Fetching in useEffect what a Server Component could render.
  • Passing data down through many levels of props: thanks to deduplication each component can fetch what it needs.
  • Awaiting all data at the top of the page, so nothing streams.

See Also​