Week 17: App Router Data Fetching, Layouts & Streaming

Week 16 established the server/client split. This week builds on it: how nested layouts avoid redundant work, how Suspense (Week 12) lets slow parts of a page stream in without blocking the fast parts, and the real question every page needs an answer to — should this render at request time, at build time, or in the browser?

Module 11 of 13 Week 17 of 20 ~3–4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Build nested layouts that avoid redundant re-fetching
  • Stream a slow section of a page without blocking the rest
  • Choose correctly between SSR, SSG and client rendering for a given page

1. Nested Layouts

A layout.tsx file wraps every page and layout nested inside its folder — and critically, a layout doesn't re-render when navigating between its child pages. Navigating from /dashboard/overview to /dashboard/settings re-renders only the changing part; the shared dashboard/layout.tsx around them stays mounted.

app/dashboard/layout.tsx
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="dashboard">
      <DashboardSidebar /> {/* stays mounted across dashboard/* navigations */}
      <main>{children}</main>
    </div>
  );
}

This matters for data fetching specifically: if DashboardSidebar fetches the current user inside the layout, that fetch runs once when the layout first mounts, not again on every navigation between dashboard sub-pages — a meaningfully different (and better) default than Week 6's <Outlet /> pattern, where re-fetching on every route change required deliberate caching (Week 8) to avoid.

2. loading.tsx & Automatic Suspense

A file named loading.tsx in any route folder is automatically wrapped around that route's page.tsx as a Suspense fallback — no manual <Suspense> JSX required, unlike Week 12's hand-wired version.

app/products/loading.tsx
export default function Loading() {
  return <ProductListSkeleton />;
}

Next.js effectively wraps app/products/page.tsx in <Suspense fallback={''}> automatically. While ProductsPage's await getProducts() (Week 16) is pending, Loading shows instead — the same underlying Suspense mechanism, now a file-naming convention instead of explicit JSX.

3. Streaming with Suspense Boundaries

A page-level loading.tsx blocks the entire page until its slowest fetch resolves. If a page has one fast piece of data and one genuinely slow one, wrapping just the slow piece in its own Suspense lets the fast content stream to the browser immediately, with the slow section's fallback showing only where that section will eventually appear.

app/products/[productId]/page.tsx
import { Suspense } from 'react';

export default async function ProductDetailPage({ params }: { params: { productId: string } }) {
  const product = await getProduct(params.productId); // fast -- awaited directly

  return (
    <div>
      <h1>{product.name}</h1> {/* streams to the browser immediately */}

      <Suspense fallback={<ReviewsSkeleton />}>
        <ProductReviews productId={product.id} /> {/* slow -- streams in separately, later */}
      </Suspense>
    </div>
  );
}

async function ProductReviews({ productId }: { productId: string }) {
  const reviews = await getReviews(productId); // a slow, separate fetch
  return <ul>{reviews.map((r: Review) => <li key={r.id}>{r.text}</li>)}</ul>;
}

The product name appears the instant its own fast fetch resolves; the reviews section's skeleton shows in place until getReviews finishes, at which point that piece of HTML streams in and swaps the skeleton out — all in a single page load, no client-side JavaScript request involved. This is the same ErrorBoundary + Suspense composition from Week 12, now streaming real server-rendered HTML instead of client-fetched data.

4. Static Generation with generateStaticParams

For dynamic routes whose full set of possible values is known ahead of time (a blog's slugs, a fixed product catalog), Next.js can render every page at build time instead of on every request — the fastest possible option, since there's no server work left to do when a real user requests the page.

app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await getAllPostSlugs(); // e.g. [{ slug: 'hello-world' }, { slug: 'react-tips' }]
  return posts; // Next.js pre-renders one static page per entry, at build time
}

export default async function BlogPostPage({ params }: { params: { slug: string } }) {
  const post = await getPostBySlug(params.slug);
  return <article>{post.content}</article>;
}

generateStaticParams tells Next.js the complete list of slug values to pre-render during next build — the resulting HTML for each is generated once and served identically to every visitor, like a plain static file, until the next deploy.

5. SSR vs. SSG vs. Client Rendering — When to Use Each

Three genuinely different rendering strategies, each right for a different shape of content:

  • SSG (Static Site Generation) — rendered once at build time. Fastest possible response, but content is only as fresh as the last deploy. Right for: blog posts, marketing pages, documentation, a product catalog that changes infrequently.
  • SSR (Server-Side Rendering) — rendered fresh on every request, on the server (the default for a Server Component without generateStaticParams). Right for: personalized content, data that must always be current, pages depending on request-specific data like cookies.
  • Client Rendering — a Client Component (Week 16) fetching with useQuery (Week 8) after the page loads. Right for: highly interactive, frequently-changing data behind an already-loaded shell — a live dashboard, a chat window, anything where Week 8–9's caching and mutation behavior is the actual point.

These aren't mutually exclusive within one app, or even one page — Section 3's example mixes an awaited server fetch (effectively SSR for that piece) with a streamed, separately-loading section, and a real page might further nest a Client Component using useMutation for a comment form. Choosing the right strategy per piece of content, not per whole app, is the actual skill.

The question to ask per page (or per section)

"Does this need to be different for every request, or could it be the same for everyone until the next deploy?" A yes to the second half of that question is a strong signal for SSG; a genuine need for per-user or always-current data points to SSR or client-side fetching instead.

6. Hands-on Exercise

Hands-on

Build a small blog: static posts, a streamed comments section

Combine static generation, layouts, and streaming in one small but realistic app.

Requirements:

  1. Create an app/blog/[slug]/page.tsx with a hardcoded array of 5 blog posts (in a data file), using generateStaticParams so every post is statically generated at build time.
  2. Add a app/blog/layout.tsx with a shared sidebar listing all post titles, and confirm — by adding a temporary console.log — that it doesn't re-run its data loading on every navigation between posts.
  3. Add a ProductReviews-style comments section to each post, deliberately delayed (~2 seconds) and wrapped in its own Suspense with a skeleton fallback, so the post content appears immediately while comments stream in after.
  4. Run next build and confirm in the build output that your blog post routes were generated as static pages, not marked for server rendering on every request.
  5. Add one genuinely dynamic page — app/dashboard/page.tsx showing the current server time on every load (no caching) — and explain in a comment why this page is a poor fit for generateStaticParams.
Hint

next build's output prints a route table with symbols indicating each route's rendering strategy (static vs. dynamic) — check the CLI output directly rather than guessing from behavior alone.

7. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

Why doesn't a shared layout.tsx re-fetch its data when navigating between two pages nested inside it?

The layout stays mounted across navigations between its child pages — only the changing page content re-renders, not the layout wrapping it. Any data fetching inside the layout itself runs once when it first mounts, not again on every navigation, which is a structural guarantee of nested layouts rather than something requiring caching to achieve.

Q2

What does a loading.tsx file do, mechanically?

Next.js automatically wraps the corresponding page.tsx in a Suspense boundary using that file as the fallback — the same underlying Suspense mechanism from Week 12, applied via a file-naming convention instead of explicit JSX you write by hand.

Q3

In the streaming example, why does the product name appear before the reviews section, without any client-side JavaScript fetch involved?

Only the reviews section is wrapped in its own Suspense boundary, so the server can stream the already-resolved product name's HTML to the browser immediately, without waiting for the separately-fetched, slower reviews data. The reviews' skeleton shows in its place until that piece of server-rendered HTML streams in later, in the same page load.

Q4

Why would a live, current-server-time dashboard be a poor fit for generateStaticParams/static generation?

Static generation renders a page once at build time and serves that same HTML to every visitor until the next deploy — exactly wrong for content that must be different on every single request. A page whose content is meaningfully different moment to moment needs SSR (rendered fresh per request) or client-side fetching, not a strategy whose whole benefit comes from content staying the same.