Week 16: Next.js — Server vs. Client Components

Everything so far has been a client-side single-page app: one HTML shell, all rendering done in the browser. Next.js changes where components actually run — some render on the server and never ship their code to the browser at all. This is the single biggest mental shift in the whole course, and this week builds it from the ground up.

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

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

  • Explain what Next.js adds on top of the React you already know
  • Navigate the App Router's file-based routing conventions
  • Decide correctly between a server component and a client component

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.

terminal
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/ directory structure
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.

app/products/[productId]/page.tsx
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.

app/products/page.tsx — a Server 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.

Server Components can't use hooks

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.

app/products/AddToCartButton.tsx
'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>
  );
}
app/products/[productId]/page.tsx — mixing both
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.
Push '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

Hands-on

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:

  1. Scaffold a new Next.js app with create-next-app and set up app/products/page.tsx and app/products/[productId]/page.tsx, following Section 2's file-based routing.
  2. Fetch your product data directly inside each async page component (simulate an API with a delay, as in earlier weeks) — no useQuery, no client-side fetch, for the base list and detail views.
  3. 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.
  4. Add a shared app/layout.tsx with a nav bar, confirming it wraps every route without needing to be repeated per page.
  5. 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.
Hint

"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?

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?

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?

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?

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.