1. What Next.js Adds
Everything through Week 15 was a client-side rendered (CSR) app: Vite ships one near-empty HTML file, and the browser downloads and runs JavaScript that builds the entire page from scratch. That means a real, if brief, blank moment before anything appears, and search engines/link previews see very little without executing JavaScript first.
Next.js is a React framework that adds server-side rendering, file-based routing, and a build/deployment pipeline on top of React itself — it doesn't replace anything you've learned; hooks, components, props and state all work exactly the same. What changes is where code runs and how routing and data fetching are wired up.
npx create-next-app@latest my-app
cd my-app
npm run dev
2. Project Setup & File-Based Routing
The App Router (Next.js's current, recommended routing system) maps
your file structure directly to URLs — no route configuration array like Week 6's
createBrowserRouter. A folder is a URL segment; specific filenames
inside it have special meaning.
app/
├── layout.tsx # root layout -- wraps every page
├── page.tsx # matches "/"
├── products/
│ ├── page.tsx # matches "/products"
│ └── [productId]/
│ └── page.tsx # matches "/products/:productId" -- dynamic segment
└── about/
└── page.tsx # matches "/about"
page.tsx defines the UI for that route; layout.tsx wraps
every page beneath it — directly analogous to Week 6's <Outlet />-based
nested layouts, except the nesting is expressed as folder nesting rather than a
route config array. [productId] — square brackets around a folder name
— is the App Router's equivalent of Week 6's :productId dynamic segment.
export default function ProductDetailPage({
params,
}: {
params: { productId: string };
}) {
return <h1>Product #{params.productId}</h1>;
}
3. Server Components by Default
This is the biggest conceptual shift: in the App Router, every component is a Server Component by default. A Server Component renders entirely on the server — its code, and any large libraries it imports, never gets sent to the browser at all. The browser receives only the resulting HTML (and a lightweight description React uses to reconcile it) — not JavaScript for that component.
async function getProducts() {
const res = await fetch('https://api.example.com/products');
return res.json();
}
export default async function ProductsPage() {
const products = await getProducts(); // runs on the SERVER, not in the browser
return (
<ul>
{products.map((p: Product) => <li key={p.id}>{p.name}</li>)}
</ul>
);
}
Notice ProductsPage is an async function component — that's
only valid for Server Components, and it means you can await data
directly in the component body, no useEffect or useQuery
needed for this kind of fetch. The generated HTML arrives already containing the
product list; there's no client-side loading spinner for this data at all, because
no client-side fetch ever happens.
useState, useEffect, useContext — none of Weeks 2–12's hooks work in a Server Component, because it never runs in a browser at all and has no concept of "re-rendering in response to state." Any interactivity needs a Client Component (Section 4).
4. The 'use client' Directive
A component needs to be a Client Component — hydrated and running
in the browser, exactly like everything from Weeks 1–15 — the moment it needs
interactivity: useState, event handlers, effects, browser-only APIs.
Opt in with a 'use client' directive at the top of the file.
'use client'; // must be the very first line -- opts this whole module into the client bundle
import { useState } from 'react';
export function AddToCartButton({ productId }: { productId: number }) {
const [added, setAdded] = useState(false);
return (
<button onClick={() => setAdded(true)}>
{added ? 'Added ✓' : 'Add to cart'}
</button>
);
}
import { AddToCartButton } from '../AddToCartButton';
export default async function ProductDetailPage({ params }: { params: { productId: string } }) {
const product = await getProduct(params.productId); // runs on the server
return (
<div>
<h1>{product.name}</h1> {/* rendered on the server, zero JS shipped for this part */}
<AddToCartButton productId={product.id} /> {/* hydrated, interactive, runs in the browser */}
</div>
);
}
This is the pattern almost every real Next.js page follows: a Server Component handles data fetching and static content, and imports small, focused Client Components only for the specific pieces that need interactivity — the button, not the whole page. Everything outside those client islands ships zero JavaScript.
5. Choosing Server vs. Client
Default to a Server Component; add 'use client' only when you hit a
genuine requirement for it:
- Stay a Server Component when: fetching data directly, reading environment secrets/API keys, rendering static or server-computed content, or including large dependencies you don't want shipped to the browser at all.
- Switch to a Client Component when: using
useState/useEffect/any hook, attaching event handlers (onClick,onChange), using browser-only APIs (localStorage,window), or using a third-party library that itself depends on hooks or browser APIs.
'use client' as far down the tree as possible
Marking your whole page 'use client' because one small button needs state defeats the entire benefit — that reverts the whole tree to the CSR model from Weeks 1–15. Extract the interactive piece into its own small client component (like AddToCartButton above) and keep everything around it as a Server Component.
6. Hands-on Exercise
Port Week 6's product catalog to the Next.js App Router
Rebuild the same feature you already know, this time drawing the server/client line deliberately.
Requirements:
- Scaffold a new Next.js app with
create-next-appand set upapp/products/page.tsxandapp/products/[productId]/page.tsx, following Section 2's file-based routing. - Fetch your product data directly inside each
asyncpage component (simulate an API with a delay, as in earlier weeks) — nouseQuery, no client-side fetch, for the base list and detail views. - Extract exactly one interactive piece — a "favorite" toggle button on each product card — into its own
'use client'component, and confirm (via a comment or a quick build-output check) that everything else on the page stays a Server Component. - Add a shared
app/layout.tsxwith a nav bar, confirming it wraps every route without needing to be repeated per page. - In your browser's DevTools, view the page source (not the rendered DOM, the actual HTML response) for the products list and confirm the product names are present in the raw HTML — proof the content was server-rendered, not just client-rendered after the fact.
"View Page Source" (not "Inspect Element") shows the actual HTML the server sent, before any client-side JavaScript runs — that's the honest test of whether something was truly server-rendered, since the rendered DOM in DevTools always looks complete either way.
7. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
What is fundamentally different about where a Server Component's code runs, compared to everything built in Weeks 1–15?
What is fundamentally different about where a Server Component's code runs, compared to everything built in Weeks 1–15?
It renders entirely on the server, and its code — along with anything it imports — is never sent to the browser at all. Everything through Week 15 ran as client-side JavaScript in the browser; a Server Component's only output the browser receives is the resulting HTML.
Q2
Why can't a Server Component use useState?
Why can't a Server Component use useState?
It never runs in a browser and is never hydrated into an interactive, re-renderable instance — it renders once, on the server, and produces static HTML. useState requires a live component instance that can re-render in response to changing state, which only exists for Client Components running in the browser.
Q3
What does the 'use client' directive at the top of a file actually do?
What does the 'use client' directive at the top of a file actually do?
It marks that module (and everything it exports) as a Client Component, meaning its code is bundled and sent to the browser to be hydrated and run there — opting that specific piece of the tree into the interactive, hook-enabled behavior React has always had, rather than the server-only rendering that's now the default.
Q4
Why is marking an entire page 'use client' just because one button needs useState considered a mistake?
Why is marking an entire page 'use client' just because one button needs useState considered a mistake?
It forces the entire page's code to ship to and run in the browser, eliminating the server-rendering benefit for all the static content on that page too — effectively reverting the whole page to the pre-Next.js CSR model. Extracting just the interactive piece into its own small client component keeps the rest of the page server-rendered and JavaScript-free.