LearnFrenzy
Sign in Take a free test

Data Fetching and Streaming · Lesson 10 of 24

Data Fetching in Next.js

8 min read Updated Nov 8, 2026 Saurabh Samir

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 fetch or 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>
  );
}

Visiting both pages

React.js: 2 reviews – loaded in about 600 ms

› open /parallel
React.js: 2 reviews – loaded in about 300 ms

Waterfall vs parallel

  1. SequentialgetCourse 300 ms → getReviews 300 ms = 600 ms
  2. ParallelgetCourse and getReviews together = 300 ms
  3. 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>
  );
}

Two visits: page and server log

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"];
}

Page

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

  1. A page needs a course, then its instructor (using the course's instructor id), 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>
      );
    }

    Page

    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.

Next lesson

Search LearnFrenzy

Esc

Quick links