Data Fetching and Streaming · Lesson 10 of 24
Data Fetching in Next.js
In the App Router, you fetch data where you need it: inside an async Server Component, with plain await. There's no special data-fetching function to learn. What you do need to understand is how requests line up – one after another or side by side – because that decides how fast your page is.
In this lesson
- Fetching with
fetchor a database in Server Components - Sequential vs parallel requests – measured
- Deduplicating calls with React's
cache - Starting a request on the server and reading it in a Client Component
fetch and databases
// app/users/page.tsx – fetching from an external API
import { Suspense } from "react";
type User = { id: number; name: string; email: string };
async function UserList() {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
if (!res.ok) throw new Error(`Failed to load users: ${res.status}`); // goes to error.tsx
const users: User[] = await res.json();
return <ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}
export default function UsersPage() {
return (
<main>
<h1>Users</h1>
<Suspense fallback={<p>Loading users…</p>}>
<UserList />
</Suspense>
</main>
);
}
With Cache Components, a fetch or database call is not cached by default – it runs at request time – so it goes inside <Suspense> (or a loading.tsx). If the data can be shared between visitors, cache it with "use cache" instead, covered in the next chapter. Querying a database works exactly the same way: const users = await db.user.findMany().
For the runnable examples in this course, data comes from a small module with artificial delays, so you can see the timings:
// lib/api.ts
import { connection } from "next/server";
const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
export async function getCourse(slug: string) {
await connection(); // request-time data, like a real database query
await wait(300);
return { slug, title: slug === "react" ? "React.js" : "Next.js", instructor: "asha" };
}
export async function getReviews(slug: string) {
await connection();
await wait(300);
return [`Great ${slug} course!`, "Clear and practical"];
}
export async function getInstructor(id: string) {
await connection();
await wait(300);
return { id, name: "Asha Verma" };
}
Sequential requests: a waterfall
// app/sequential/page.tsx
import { Suspense } from "react";
import { getCourse, getReviews } from "@/lib/api";
async function CourseInfo() {
const start = performance.now();
const course = await getCourse("react"); // waits 300 ms…
const reviews = await getReviews("react"); // …then another 300 ms
const ms = Math.round((performance.now() - start) / 100) * 100;
return <p>{course.title}: {reviews.length} reviews – loaded in about {ms} ms</p>;
}
export default function Page() {
return (
<Suspense fallback={<p>Loading…</p>}>
<CourseInfo />
</Suspense>
);
}
Parallel requests
The two requests don't depend on each other, so start both and wait for them together:
// app/parallel/page.tsx
import { Suspense } from "react";
import { getCourse, getReviews } from "@/lib/api";
async function CourseInfo() {
const start = performance.now();
const [course, reviews] = await Promise.all([getCourse("react"), getReviews("react")]); // both at once
const ms = Math.round((performance.now() - start) / 100) * 100;
return <p>{course.title}: {reviews.length} reviews – loaded in about {ms} ms</p>;
}
export default function Page() {
return (
<Suspense fallback={<p>Loading…</p>}>
<CourseInfo />
</Suspense>
);
}
React.js: 2 reviews – loaded in about 600 ms › open /parallel React.js: 2 reviews – loaded in about 300 ms
Waterfall vs parallel
- SequentialgetCourse 300 ms → getReviews 300 ms = 600 ms
- ParallelgetCourse and getReviews together = 300 ms
- Ruleonly wait in sequence when a request needs the previous result (e.g. the instructor id from the course)
Waterfalls also happen across components: if a parent awaits data and then renders a child that awaits more data, the child's request can't start until the parent's has finished. Start independent requests early – or give each component its own Suspense so they load side by side (next lesson).
Deduplicate with React cache
Often the same data is needed in several places – the page title, the header and the body all want the current course. Wrap the function in React's cache and it runs only once per request, however many components call it:
// lib/course.ts
import { cache } from "react";
import { connection } from "next/server";
let calls = 0;
export const getCourse = cache(async (slug: string) => {
await connection();
calls++;
console.log(`database query #${calls} for ${slug}`);
return { slug, title: "React.js", lessons: 31 };
});
// app/page.tsx
import { Suspense } from "react";
import { getCourse } from "@/lib/course";
async function Title() {
const course = await getCourse("react");
return <h1>{course.title}</h1>;
}
async function LessonCount() {
const course = await getCourse("react"); // same request → no second query
return <p>{course.lessons} lessons</p>;
}
export default function Page() {
return (
<Suspense fallback={<p>Loading…</p>}>
<Title />
<LessonCount />
</Suspense>
);
}
React.js 31 lessons server: database query #1 for react › reload the page React.js 31 lessons server: database query #2 for react
One query per visit, even though two components asked for the data. The memo is per request – the next visitor triggers a fresh query. (To share results between requests you need real caching with "use cache".)
Start on the server, read on the client
A Server Component can start a request without awaiting it and pass the promise to a Client Component, which reads it with React's use. The page shell renders immediately and the data streams in:
// app/reviews.tsx
"use client";
import { use, useState } from "react";
export function Reviews({ reviewsPromise }: { reviewsPromise: Promise<string[]> }) {
const reviews = use(reviewsPromise); // suspends until the promise resolves
const [showAll, setShowAll] = useState(false);
const shown = showAll ? reviews : reviews.slice(0, 1);
return (
<section>
<ul>{shown.map((r) => <li key={r}>{r}</li>)}</ul>
{!showAll && <button onClick={() => setShowAll(true)}>Show all {reviews.length}</button>}
</section>
);
}
// app/page.tsx
import { Suspense } from "react";
import { getReviews } from "@/lib/api";
import { Reviews } from "./reviews";
export default function Page() {
const reviewsPromise = getReviews("react"); // started, not awaited
return (
<main>
<h1>React.js reviews</h1>
<Suspense fallback={<p>Loading reviews…</p>}>
<Reviews reviewsPromise={reviewsPromise} />
</Suspense>
</main>
);
}
// lib/api.ts
import { connection } from "next/server";
export async function getReviews(slug: string) {
await connection();
await new Promise((r) => setTimeout(r, 300));
return [`Great ${slug} course!`, "Clear and practical", "Loved the projects"];
}
React.js reviews • Great react course! [Show all 3] › click “Show all 3” React.js reviews • Great react course! • Clear and practical • Loved the projects
Client-side fetching still has its place
Fetch on the server by default. Use client-side fetching (SWR or TanStack Query, calling a Route Handler) for data that changes while the user is on the page – live search suggestions, infinite scroll, polling a job's status.
Practice
Try it yourself
A page needs a course, then its instructor (using the course's
instructorid), and the reviews. Load them as fast as possible.Show solution
// lib/api.ts import { connection } from "next/server"; const wait = (ms: number) => new Promise((r) => setTimeout(r, ms)); export async function getCourse(slug: string) { await connection(); await wait(300); return { slug, title: "React.js", instructor: "asha" }; } export async function getReviews(slug: string) { await connection(); await wait(300); return [`Great ${slug} course!`]; } export async function getInstructor(id: string) { await connection(); await wait(300); return { id, name: "Asha Verma" }; }// app/page.tsx import { Suspense } from "react"; import { getCourse, getInstructor, getReviews } from "@/lib/api"; async function CourseInfo() { const start = performance.now(); const reviewsPromise = getReviews("react"); // independent: start now const course = await getCourse("react"); const instructor = await getInstructor(course.instructor); // depends on course const reviews = await reviewsPromise; // already finished meanwhile const ms = Math.round((performance.now() - start) / 100) * 100; return <p>{course.title} by {instructor.name}, {reviews.length} review – about {ms} ms</p>; } export default function Page() { return ( <Suspense fallback={<p>Loading…</p>}> <CourseInfo /> </Suspense> ); }React.js by Asha Verma, 1 review – about 600 ms
600 ms instead of 900: the reviews load while the course and instructor load in sequence.
Quick check
Is fetch cached by default with Cache Components?
No – it runs at request time unless you use "use cache".
How do you run two independent requests at once?
Start both, then await Promise.all([...]).
How long does React's cache() remember a result?
For the current request only.
Summary
- Fetch with await in async Server Components; request-time data goes inside Suspense.
- Avoid waterfalls: start independent requests together with Promise.all.
- React cache() deduplicates within a request; promises can be passed to Client Components and read with use().
Finished this lesson?
Mark it complete to track your progress, then carry on with the next lesson.