Data Fetching in Server Components
Fetching with async/await directly in components, and the difference between sequential and parallel requests.
3 phút đọc
Because Server Components can be async functions, data fetching in Next.js often looks like nothing more than plain await — no useEffect, no loading-state juggling for the initial render, no client-side data library required for the common case.
Fetching with fetch
// app/blog/page.tsx
export default async function BlogPage() {
const res = await fetch("https://api.example.com/posts");
const posts = await res.json();
return (
<ul>
{posts.map((post: { id: string; title: string }) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}This runs entirely on the server. The browser never makes this request — it only receives the finished HTML (and later, the data needed for client-side transitions). fetch calls in Next.js are also automatically memoized for the duration of a single render: if a layout and a page both fetch the exact same URL with the same options, only one actual network request happens.
Fetching with a database client or ORM
There's nothing fetch-specific about server data fetching — any async I/O works, including a direct database query:
import { db, posts } from "@/lib/db";
export default async function BlogPage() {
const allPosts = await db.select().from(posts);
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}This is safe specifically because it's a Server Component — the query, the connection string, and any driver internals never reach the browser bundle.
Sequential vs. parallel fetching
Two separate awaits in the same function run one after another — the second doesn't start until the first finishes:
// Sequential: getAlbums waits for getArtist to finish first, even
// though the two calls are unrelated
const artist = await getArtist(username);
const albums = await getAlbums(username);If the two requests don't depend on each other's results, starting them together and awaiting both is faster — the calls to fetch begin as soon as they're invoked, and Promise.all just waits for both to land:
export default async function Page({
params,
}: {
params: Promise<{ username: string }>;
}) {
const { username } = await params;
// Both requests start immediately, in parallel
const artistPromise = getArtist(username);
const albumsPromise = getAlbums(username);
const [artist, albums] = await Promise.all([artistPromise, albumsPromise]);
return (
<>
<h1>{artist.name}</h1>
<Albums list={albums} />
</>
);
}Sequential fetching is sometimes unavoidable — you may genuinely need an artist's ID before you can look up their albums. In that case, wrapping the dependent part in its own component and streaming it behind a <Suspense> boundary (covered in the loading and streaming lesson) keeps the rest of the page from being blocked by it.
Deduplicating non-fetch calls with React.cache
fetch's automatic memoization doesn't extend to database clients or other non-fetch async functions. Wrap those in React's cache() to get the same effect — multiple calls within one render share a single result instead of hitting the database repeatedly:
// lib/user.ts
import { cache } from "react";
export const getUser = cache(async (id: string) => {
return db.query.users.findFirst({ where: (u, { eq }) => eq(u.id, id) });
});Any Server Component that calls getUser(id) during the same request reuses the first result rather than issuing a new query — handy when both a layout and a page need the same user record.