frontend / react
Server Components vs Client Components
Explication
Ce que vous allez apprendre
- Comprendre pourquoi tout le code React n'a pas besoin d'être exécuté côté navigateur
- Distinguer un Server Component d'un Client Component et leurs capacités respectives
- Utiliser la directive
"use client"pour marquer la frontière entre les deux mondes - Composer un Server Component avec un Client Component via des props ou le slot pattern
- Garder les secrets et accès base de données confinés au serveur
Dans quel contexte ?
Sur src/app/cours/[id]/page.jsx (Next.js App Router), un développeur doit afficher le contenu d'un cours récupéré directement en base de données, tout en gardant un bouton "Ajouter aux favoris" interactif. Faire de toute la page un Client Component obligerait à passer par une route API intermédiaire juste pour lire la base de données, alors qu'un Server Component peut y accéder directement. La solution : la page reste un Server Component, et seul le bouton favori devient un Client Component isolé avec "use client".
Étape 1 : tout s'exécutait côté navigateur jusqu'ici
Jusqu'ici dans ce cours, tout composant React était exécuté dans le navigateur de l'utilisateur : son code JavaScript devait être téléchargé, puis exécuté côté client.
Étape 2 : les deux problèmes que ça pose
Cela pose deux problèmes à grande échelle : chaque composant alourdit le bundle JavaScript, et un composant qui a besoin de données doit passer par une API intermédiaire, même sans interactivité réelle.
Étape 3 : la solution, les Server Components
Les React Server Components (RSC) sont des composants qui s'exécutent EXCLUSIVEMENT sur le serveur, et dont le code ne rejoint jamais le bundle JavaScript envoyé au navigateur.
Étape 4 : ce qu'ils gagnent, ce qu'ils perdent
Ils peuvent accéder directement à une base de données sans jamais l'exposer côté client. En contrepartie, ils ne peuvent avoir aucune interactivité : pas de useState, pas de useEffect.
Étape 5 : et pour l'interactivité, "use client"
Un Client Component est, à l'inverse, un composant classique qui s'exécute aussi dans le navigateur. La directive "use client" marque explicitement la frontière entre les deux mondes.
Étape 6 : comment les deux collaborent
Un Server Component peut englober un Client Component en lui transmettant des données déjà résolues via des props classiques, jamais une connexion à une base de données.
Un dernier cas particulier
L'inverse n'est pas possible directement : un Client Component ne peut pas importer un Server Component, sauf en le recevant via children, le "slot pattern".
Piège fréquent
Ajouter "use client" en haut d'un fichier qui importe une clé d'API secrète ou une connexion base de données expose ce code (et potentiellement ces secrets) au bundle JavaScript envoyé au navigateur. Garde toujours les accès sensibles dans des Server Components, jamais dans un fichier marqué "use client".
| Besoin du composant | Type à choisir |
|---|---|
| Lire directement une base de données | Server Component |
| Gérer un clic, un state, un formulaire | Client Component |
| Afficher du contenu statique sans interaction | Server Component |
Et ensuite ?
Une fois cette distinction comprise, la prochaine étape explore les approches modernes de récupération de données.
Commandes & code
Server Components vs Client Components
React Server Components (RSC) exécutent du code EXCLUSIVEMENT sur le serveur, sans jamais rejoindre le bundle client.
// Server Component (défaut dans un framework type Next.js App Router, pas de directive nécessaire)
// FichierListeCours.jsx -- s'exécute UNIQUEMENT côté serveur
async function ListeCours() {
// accès direct à la base de données ou à un service interne : jamais exposé au client
const cours = await db.cours.findMany({ where: { publie: true } });
return (
<ul>
{cours.map((c) => (
<li key={c.id}>{c.titre}</li>
))}
</ul>
);
}
// aucun useState/useEffect/onClick possible ici : pas d'interactivité, pas de bundle JS envoyé// Client Component : directive explicite obligatoire, exécuté aussi côté navigateur
"use client";
import { useState } from "react";
function BoutonFavori({ coursId, estFavoriInitial }) {
const [estFavori, setEstFavori] = useState(estFavoriInitial);
async function basculer() {
setEstFavori(!estFavori); // interactivité -> nécessite le JS client
await fetch(`/api/favoris/${coursId}`, { method: "POST" });
}
return (
<button onClick={basculer}>
{estFavori ? "Favori" : "Ajouter aux favoris"}
</button>
);
}// Composition : un Server Component peut englober un Client Component (mais pas l'inverse directement)
async function PageCours({ id }) {
const cours = await db.cours.findUnique({ where: { id } }); // fetch côté serveur
return (
<article>
<h1>{cours.titre}</h1>
<p>{cours.description}</p>
{/* Client Component reçoit des données déjà résolues en props, pas la connexion DB */}
<BoutonFavori coursId={cours.id} estFavoriInitial={cours.estFavori} />
</article>
);
}
// Passer un Server Component EN CHILDREN d'un Client Component reste possible :
// c'est le "slot pattern", utile pour garder de l'interactivité en frontière
"use client";
function CarteAvecAnimation({ children }) {
const [survole, setSurvole] = useState(false);
return (
<div onMouseEnter={() => setSurvole(true)} className={survole ? "carte--hover" : "carte"}>
{children} {/* peut être un Server Component rendu par le parent */}
</div>
);
}| Server Component | Client Component |
|---|---|
| Accès direct DB/fichiers/secrets | Interactivité (state, événements) |
| Zéro JS envoyé au navigateur | Nécessite "use client" |
async function autorisée directement | useEffect pour les effets |
Pas de hooks (useState, etc.) | Hooks complets disponibles |
Résumé
- Un Server Component ne peut ni utiliser de hooks, ni gérer d'événements : il rend du HTML/RSC payload, un point c'est tout.
"use client"marque la frontière : tout ce qui suit (et ses imports) rejoint le bundle JavaScript client.- Un Server Component peut englober un Client Component, jamais l'inverse directement (sauf via
children). - Les données sensibles (clés API, requêtes DB) restent confinées aux Server Components, jamais exposées au client.
Exercices pratiques
Mission : rapatrier une clé secrète hors du bundle client
Objectif : Corriger une fuite de secret côté client causée par un mauvais placement de "use client", et concevoir la frontière serveur/client correcte.
Contexte
Un audit de sécurité repère, dans le bundle JavaScript envoyé au navigateur, la chaîne littérale d'une clé d'API de paiement. En creusant src/app/cours/[id]/page.jsx, le fichier commence par "use client"; tout en haut, puis importe STRIPE_SECRET_KEY depuis un fichier de config et fait await db.cours.findUnique(...) directement dans le composant, à côté d'un bouton "Ajouter aux favoris" qui a besoin de useState pour son état visuel.