Pourquoi un rendu serveur
Une application React créée avec Vite envoie au navigateur une page HTML presque vide et un gros fichier JavaScript : c’est le navigateur qui construit l’interface. Cela pose trois problèmes : le premier affichage attend le téléchargement du JavaScript, les moteurs de recherche voient moins bien le contenu, et toutes les données transitent par des appels d’API depuis le client.
Next.js est un framework React qui produit le HTML côté serveur. Le navigateur reçoit une page déjà remplie, puis React « hydrate » les parties interactives. Ce cours utilise l’App Router (dossier app/), le routeur recommandé depuis Next.js 13.
npx create-next-app@latest ma-boutique # Node.js 20.9 minimum, LTS 26 conseillée
cd ma-boutique
npm run dev # http://localhost:3000
Avec les réponses par défaut, vous obtenez Next.js 16.4 et React 19.3, en TypeScript, avec l’App Router, Tailwind CSS 4, ESLint et le compilateur Turbopack. Le fichier next.config.ts généré active cacheComponents: true : c’est le modèle de rendu et de cache de Next.js 16, celui de ce cours.
Les modes de rendu
Avec Cache Components, Next.js essaie de tout prérendre au next build. Ce qui dépend de la requête est rendu à la demande, et seulement cette partie :
| Mode | Quand le HTML est produit | Symbole | Exemple |
|---|---|---|---|
| Statique | une fois, au build | ○ |
page « À propos », mentions légales |
| Prérendu partiel | un shell au build, le reste à chaque requête | ◐ |
espace client : en-tête fixe, salutation personnalisée |
| Régénération incrémentale (ISR) | au build, puis régénéré en arrière-plan (leçon 3) | ○ + durée |
catalogue produits |
| Dynamique | entièrement à la demande | ƒ |
API qui lit la requête (leçon 4) |
Une donnée propre à la requête (cookies(), headers(), les searchParams de l’URL, ou connection() qui signale explicitement « à rendre à la requête ») se lit dans un composant placé sous une frontière <Suspense>. Au build, Next.js produit le HTML de tout ce qui est au-dessus, avec le fallback à la place de la partie dynamique : c’est le shell statique. À chaque requête, il envoie ce shell aussitôt puis la partie dynamique dans la même réponse. C’est le prérendu partiel (Partial Prerendering) :
// app/compte/page.tsx
import { Suspense } from "react";
import { cookies } from "next/headers";
async function Salutation() {
const session = (await cookies()).get("session"); // donnée de requête
return <p>{session ? "Bonjour" : "Invité"}</p>;
}
export default function Compte() {
return (
<main>
<h1>Mon compte</h1> {/* dans le shell statique */}
<Suspense fallback={<p>Chargement…</p>}>
<Salutation /> {/* rendu à chaque requête */}
</Suspense>
</main>
);
}
Si la page lit cookies() hors de tout <Suspense>, next build échoue : « Next.js encountered uncached or runtime data during prerendering », avec les correctifs possibles (ajouter <Suspense>, mettre la donnée en cache avec "use cache"). De même, une valeur qui change à chaque rendu, comme new Date() ou Math.random(), est refusée pendant le prérendu : on la met en cache, ou on la calcule à la requête après await connection(), sous <Suspense>.
La commande next build affiche le mode de chaque route avec ces symboles et leur légende.
Composants serveur par défaut
Dans app/, tout composant est un composant serveur par défaut. Il s’exécute sur le serveur, peut être async et lire des données directement (base de données, API, fichier), et son code n’est jamais envoyé au navigateur.
// app/produits/page.tsx — composant serveur
import { listerProduits } from "@/lib/catalogue";
export default async function PageProduits() {
const produits = await listerProduits(); // requête SQL ou appel d'API, côté serveur
return (
<ul>
{produits.map((p) => (
<li key={p.id}>{p.nom} : {p.prix} €</li>
))}
</ul>
);
}
Une clé secrète utilisée ici reste sur le serveur. En revanche, un composant serveur ne peut pas utiliser useState, useEffect ni onClick : il n’existe pas dans le navigateur.
Composants client et directive “use client”
Pour l’interactivité, on écrit un composant client : un fichier qui commence par la directive "use client".
// 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 ? "Retirer" : "Ajouter"} {nom} aux favoris
</button>
);
}
La directive marque une frontière : ce fichier et tout ce qu’il importe partent dans le JavaScript du navigateur. Un composant client est quand même prérendu en HTML sur le serveur, puis hydraté.
Règle d’or : placez "use client" le plus bas possible dans l’arbre. La page reste serveur, seul le bouton devient client :
// app/produits/page.tsx (serveur) importe un îlot client
import { BoutonFavori } from "./BoutonFavori";
// … <li key={p.id}>{p.nom} <BoutonFavori nom={p.nom} /></li>
Ce qui traverse la frontière
Les props passées d’un composant serveur à un composant client sont sérialisées pour voyager jusqu’au navigateur. Chaînes, nombres, booléens, tableaux et objets simples passent ; une fonction ne passe pas (sauf une Server Action, leçon 4). Un composant client ne peut pas non plus importer un composant serveur, mais il peut le recevoir déjà rendu via children :
<Accordeon> {/* composant client */}
<DetailsProduit /> {/* composant serveur passé en children */}
</Accordeon>
Le streaming
Le rendu serveur est découpé par les frontières <Suspense> : Next.js envoie d’abord le shell (en-tête, navigation, fallback), puis chaque morceau dès que ses données sont prêtes, dans le même flux HTML. L’utilisateur voit la page tout de suite, même si une partie lente charge encore.
À retenir
- Statique
○, prérendu partiel◐, ISR ou dynamiqueƒ:next buildvous dit lequel. - Données de requête sous
<Suspense>, sinon le build échoue. - Composant serveur par défaut : données et secrets côté serveur, zéro JavaScript envoyé.
"use client"pour l’état et les événements, au plus bas de l’arbre, avec des props sérialisables.
Exercices
Les solutions et les tests restent sur le serveur. Dans l'espace étudiant, ton code est exécuté et corrigé, et le tuteur donne des indices avant la solution.
Choisir le mode de rendu (modèle simplifié)
Écrivez
modeDeRendu(route)qui reproduit, de façon simplifiée, la décision de Next.js 16 avec Cache Components.routeest un objet :lectures: tableau des données de requête lues par la page ("cookies","headers","searchParams","connection","params"), éventuellement vide ;dansSuspense:truesi ces lectures se font dans un composant placé sous<Suspense>;staticParams:truesi la page exportegenerateStaticParams(lesparamssont alors connus au build et ne comptent pas comme une lecture de requête) ;revalidate: la duréerevalidate(en secondes) ducacheLifed’une page mise en cache avec"use cache", ouundefined.
Règles, dans l’ordre :
- s’il reste au moins une lecture de requête :
"partiel"si elle est sous<Suspense>, sinon"erreur"(le build échoue) ; revalidatenombre strictement positif →"isr";- sinon →
"statique".
function modeDeRendu(route) { // TODO }Lire la sortie de next build
Créez un projet avec
npx create-next-app@latest rendu-demoen acceptant les réponses par défaut (TypeScript, ESLint, Tailwind CSS, App Router, sans dossiersrc). Ajoutez deux pages :app/a-propos/page.tsxqui renvoie<h1>À propos</h1>;app/compte/page.tsx: un<h1>Mon compte</h1>suivi d’un<Suspense fallback={<p>Chargement…</p>}>qui entoure un composantasyncSalutation. Celui-ci lit(await cookies()).get("session")(import depuisnext/headers) et afficheBonjoursi le cookie existe,Invitésinon.
Lancez
npm run buildet lisez le tableau des routes affiché à la fin. Déplacez ensuite la lecture decookies()directement dans la page, hors du<Suspense>, relancez le build et lisez l’erreur, puis revenez à la version avec<Suspense>.- La compilation se termine sans erreur
- La route /a-propos est marquée du symbole ○ (Static)
- La route /compte est marquée du symbole ◐ (Partial Prerender)
- Sans <Suspense>, next build échoue en signalant une donnée de requête lue pendant le prérendu
Où placer "use client" ?
Une fiche produit affiche un titre, une description et un avis client chargés côté serveur, plus un bouton « Ajouter au panier » qui met à jour un compteur avec
useState. Où placer la directive"use client"?- En tête de app/produits/[slug]/page.tsx
- Uniquement en tête du fichier du bouton Ajouter au panier
- Dans le layout racine pour que tout fonctionne
- Nulle part : useState fonctionne dans un composant serveur
Quiz de fin de leçon
Quel est le type d’un composant dans app/ sans directive particulière ?
- Composant client
- Composant serveur
- Composant statique sans rendu
Avec Cache Components, que se passe-t-il si une page lit cookies() hors de tout
? - La page devient dynamique sans autre message
- next build échoue et propose d'ajouter <Suspense> ou "use cache"
- Le cookie est ignoré au build
Pourquoi ne peut-on pas passer onClick={() => …} d’un composant serveur à un composant client ?
- Les fonctions ne sont pas sérialisables
- onClick est réservé aux balises HTML
- Next.js interdit les fonctions fléchées
La suite du cours
- Routage avec l'App Router20 min
- Charger les données et maîtriser le cache22 min
- API intégrées : Route Handlers et Server Actions24 min
- Construire et déployer une application Next.js20 min