Why server rendering
A React application created with Vite sends the browser an almost empty HTML page and a large JavaScript file: it is the browser that builds the interface. This causes three problems: the first paint waits for the JavaScript to download, search engines see the content less well, and all data goes through API calls from the client.
Next.js is a React framework that produces the HTML on the server. The browser receives an already filled page, then React “hydrates” the interactive parts. This course uses the App Router (app/ folder), the recommended router since Next.js 13.
npx create-next-app@latest ma-boutique # Node.js 20.9 minimum, LTS 26 recommended
cd ma-boutique
npm run dev # http://localhost:3000
With the default answers, you get Next.js 16.4 and React 19.3, in TypeScript, with the App Router, Tailwind CSS 4, ESLint, and the Turbopack compiler. The generated next.config.ts enables cacheComponents: true: this is the rendering and caching model of Next.js 16, the one used in this course.
Rendering modes
With Cache Components, Next.js tries to prerender everything at next build. What depends on the request is rendered on demand, and only that part:
| Mode | When the HTML is produced | Symbol | Example |
|---|---|---|---|
| Static | once, at build time | ○ |
“About” page, legal notices |
| Partial prerendering | a shell at build time, the rest on each request | ◐ |
customer area: fixed header, personalized greeting |
| Incremental regeneration (ISR) | at build time, then regenerated in the background (lesson 3) | ○ + duration |
product catalog |
| Dynamic | entirely on demand | ƒ |
API that reads the request (lesson 4) |
Request-specific data (cookies(), headers(), the URL’s searchParams, or connection() which explicitly signals “render at request time”) is read in a component placed under a <Suspense> boundary. At build time, Next.js produces the HTML of everything above it, with the fallback in place of the dynamic part: this is the static shell. On each request, it sends this shell right away and then the dynamic part in the same response. This is partial prerendering (Partial Prerendering):
// app/compte/page.tsx
import { Suspense } from "react";
import { cookies } from "next/headers";
async function Salutation() {
const session = (await cookies()).get("session"); // request data
return <p>{session ? "Bonjour" : "Invité"}</p>;
}
export default function Compte() {
return (
<main>
<h1>Mon compte</h1> {/* in the static shell */}
<Suspense fallback={<p>Chargement…</p>}>
<Salutation /> {/* rendered on each request */}
</Suspense>
</main>
);
}
If the page reads cookies() outside any <Suspense>, next build fails: “Next.js encountered uncached or runtime data during prerendering”, with possible fixes (add <Suspense>, cache the data with "use cache"). Likewise, a value that changes on every render, like new Date() or Math.random(), is rejected during prerendering: you cache it, or compute it at request time after await connection(), under <Suspense>.
The next build command displays each route’s mode with these symbols and their legend.
Server Components by default
In app/, every component is a Server Component by default. It runs on the server, can be async and read data directly (database, API, file), and its code is never sent to the browser.
// app/produits/page.tsx — Server Component
import { listerProduits } from "@/lib/catalogue";
export default async function PageProduits() {
const produits = await listerProduits(); // SQL query or API call, server side
return (
<ul>
{produits.map((p) => (
<li key={p.id}>{p.nom} : {p.prix} €</li>
))}
</ul>
);
}
A secret key used here stays on the server. However, a Server Component cannot use useState, useEffect, or onClick: it doesn’t exist in the browser.
Client Components and the “use client” directive
For interactivity, you write a Client Component: a file that starts with the "use client" directive.
// app/produits/BoutonFavori.tsx
"use client";
import { useState } from "react";
export function BoutonFavori({ nom }: { nom: string }) {
const [actif, setActif] = useState(false);
return (
<button aria-pressed={actif} onClick={() => setActif(!actif)}>
{actif ? "Remove" : "Add"} {nom} to favorites
</button>
);
}
The directive marks a boundary: this file and everything it imports go into the browser’s JavaScript. A Client Component is still prerendered to HTML on the server, then hydrated.
Golden rule: place "use client" as low as possible in the tree. The page stays on the server, only the button becomes client:
// app/produits/page.tsx (server) imports a client island
import { BoutonFavori } from "./BoutonFavori";
// … <li key={p.id}>{p.nom} <BoutonFavori nom={p.nom} /></li>
What crosses the boundary
Props passed from a Server Component to a Client Component are serialized to travel to the browser. Strings, numbers, booleans, arrays, and simple objects pass; a function does not (except a Server Action, lesson 4). A Client Component also cannot import a Server Component, but it can receive one already rendered via children:
<Accordeon> {/* Client Component */}
<DetailsProduit /> {/* Server Component passed as children */}
</Accordeon>
Streaming
Server rendering is split up by the <Suspense> boundaries: Next.js first sends the shell (header, navigation, fallback), then each piece as soon as its data is ready, in the same HTML stream. The user sees the page right away, even if a slow part is still loading.
Key takeaways
- Static
○, partial prerendering◐, ISR or dynamicƒ:next buildtells you which. - Request data under
<Suspense>, otherwise the build fails. - Server Component by default: data and secrets on the server, zero JavaScript sent.
"use client"for state and events, at the bottom of the tree, with serializable props.
Exercises
Solutions and tests stay on the server. In the student space, your code is run and checked, and the tutor gives hints before the solution.
Choose the rendering mode (simplified model)
Write
modeDeRendu(route)which reproduces, in a simplified way, the Next.js 16 decision with Cache Components.routeis an object:lectures: array of the request data read by the page ("cookies","headers","searchParams","connection","params"), possibly empty;dansSuspense:trueif these reads happen in a component placed under<Suspense>;staticParams:trueif the page exportsgenerateStaticParams(theparamsare then known at build time and do not count as a request read);revalidate: therevalidateduration (in seconds) of thecacheLifeof a page cached with"use cache", orundefined.
Rules, in order:
- if at least one request read remains:
"partiel"if it is under<Suspense>, otherwise"erreur"(the build fails); revalidatea strictly positive number →"isr";- otherwise →
"statique".
function modeDeRendu(route) { // TODO }Read the output of next build
Create a project with
npx create-next-app@latest rendu-demo, accepting the default answers (TypeScript, ESLint, Tailwind CSS, App Router, nosrcfolder). Add two pages:app/a-propos/page.tsxthat returns<h1>À propos</h1>;app/compte/page.tsx: an<h1>Mon compte</h1>followed by a<Suspense fallback={<p>Chargement…</p>}>that wraps anasynccomponentSalutation. It reads(await cookies()).get("session")(imported fromnext/headers) and displaysBonjourif the cookie exists,Invitéotherwise.
Run
npm run buildand read the routes table displayed at the end. Then move thecookies()read directly into the page, outside the<Suspense>, rerun the build and read the error, then go back to the version with<Suspense>.- The build finishes without errors
- The /a-propos route is marked with the ○ (Static) symbol
- The /compte route is marked with the ◐ (Partial Prerender) symbol
- Without <Suspense>, next build fails, reporting request data read during prerendering
Where should "use client" go?
A product page displays a title, a description, and a customer review loaded on the server, plus an “Add to cart” button that updates a counter with
useState. Where should the"use client"directive go?- At the top of app/produits/[slug]/page.tsx
- Only at the top of the Add to cart button's file
- In the root layout so that everything works
- Nowhere: useState works in a Server Component
End-of-lesson quiz
What type is a component in app/ with no particular directive?
- Client Component
- Server Component
- Static component with no rendering
With Cache Components, what happens if a page reads cookies() outside any
? - The page becomes dynamic with no other message
- next build fails and suggests adding <Suspense> or "use cache"
- The cookie is ignored at build time
Why can’t you pass onClick={() => …} from a Server Component to a Client Component?
- Functions are not serializable
- onClick is reserved for HTML tags
- Next.js forbids arrow functions
The rest of the course
- Routing with the App Router20 min
- Loading Data and Mastering the Cache22 min
- Built-in APIs: Route Handlers and Server Actions24 min
- Building and Deploying a Next.js Application20 min