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.
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.
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.
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.
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.
"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
Build a small blog: static posts, a streamed comments section
Combine static generation, layouts, and streaming in one small but realistic app.
Requirements:
- Create an
app/blog/[slug]/page.tsxwith a hardcoded array of 5 blog posts (in a data file), usinggenerateStaticParamsso every post is statically generated at build time. - Add a
app/blog/layout.tsxwith a shared sidebar listing all post titles, and confirm — by adding a temporaryconsole.log— that it doesn't re-run its data loading on every navigation between posts. - Add a
ProductReviews-style comments section to each post, deliberately delayed (~2 seconds) and wrapped in its ownSuspensewith a skeleton fallback, so the post content appears immediately while comments stream in after. - Run
next buildand confirm in the build output that your blog post routes were generated as static pages, not marked for server rendering on every request. - Add one genuinely dynamic page —
app/dashboard/page.tsxshowing the current server time on every load (no caching) — and explain in a comment why this page is a poor fit forgenerateStaticParams.
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?
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?
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?
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?
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.