Server et Client Components
Voici le module du bloc Next.js. Pas le plus long, pas le plus technique : le plus structurant. Tout ce qui suit — le routing, le rendu, les Server Actions — n'est que la conséquence d'une seule idée, et cette idée tient en une question que tu devras te poser devant chaque fichier d'un projet Next.js : « où est-ce que ce code s'exécute ? » Prends ton temps ici, relis-le. Beaucoup de texte et peu de code, délibérément : le code n'est que l'illustration d'un déplacement mental.
Jusqu'à maintenant, dans tout ce que tu as écrit — React Native au quotidien, React web dans le module « React web ↔ React Native » — il n'existait qu'un seul lieu où ton code tournait : l'appareil sur mobile, le navigateur sur le web classique. Un seul lieu, donc jamais de question à se poser. Next.js casse ça : dans l'App Router, ton code React vit dans deux lieux, et c'est toi qui décides lequel, fichier par fichier.
Attention à la formulation exacte, parce que c'est là que tout le monde se trompe. La bonne opposition n'est pas « serveur ou client », c'est « serveur seulement » contre « serveur PUIS client » : un Server Component s'exécute une fois sur le serveur et n'envoie jamais son code au navigateur ; un Client Component est lui aussi exécuté sur le serveur pour produire le HTML initial, puis reprend vie dans le navigateur pour y devenir interactif. Tant que tu ne sais pas où tourne le fichier que tu lis, la moitié des erreurs de Next.js te semblera arbitraire ; une fois que tu le sais, elles deviennent prévisibles avant même de lancer le projet.
Le module « La carte de Next.js » t'a donné le plan du territoire. Celui-ci zoome sur le mécanisme central ; les trois suivants — « Routing », « Rendu & data fetching » et « Route Handlers & Server Actions » — supposent tous que la distinction serveur / client est acquise. C'est ici que ça se joue.
Le problème que ça résout : quand tout partait dans le navigateur
Pour comprendre pourquoi les Server Components existent, il faut d'abord ressentir la douleur qu'ils soignent. Une application React « classique » — celle que tu construirais avec Vite, ou celle que tu connais déjà sous forme d'app React Native — fonctionne selon un schéma unique : tout le JavaScript est téléchargé par le navigateur, puis exécuté par le navigateur. Le serveur n'est qu'un distributeur de fichiers : il envoie une page HTML quasiment vide, un gros paquet de JavaScript, et il se retire.
Prenons notre fil rouge, RendezVous : une application de prise de rendez-vous en clinique, avec une page qui affiche les services offerts et un formulaire pour réserver un créneau. Voici ce qui se passe, seconde par seconde, quand un patient ouvre cette page dans la version « tout client ».
-
Le navigateur demande la page. Le serveur répond avec un fichier HTML
qui contient, en gros,
<div id="root"></div>et une balise<script>. L'utilisateur voit… une page blanche. - Le navigateur télécharge le JavaScript. Pas seulement ton code : React, le routeur, la bibliothèque de formulaires, celle de dates, les utilitaires… Sur un réseau mobile moyen, ça se compte en secondes. Toujours la page blanche.
- Le navigateur exécute le JavaScript. React démarre, monte l'arbre, et la page affiche enfin… un squelette vide, faute de données. Un spinner à la place de la liste des services.
-
Maintenant seulement, un
useEffectlance unfetchvers l'API pour récupérer les services : un aller-retour réseau de plus. Puis un deuxièmefetchpour les disponibilités du praticien, qui ne peut partir qu'après le premier puisqu'il a besoin de l'identifiant du service. Deux allers-retours en file indienne. - Enfin, les données arrivent, l'état change, React re-render, et la liste s'affiche.
Les étapes 1 et 3 devraient te dire quelque chose : ce
<div id="root"></div> vide, ce <script>, puis React qui
« monte l'arbre » — c'est Le clou que Next.js enfonce à ta place, du module
React web ↔ React Native : les ponts, autrement dit
createRoot(document.getElementById('root')).render(<App />). Dans le schéma
tout-client, c'est toi qui plantes ce clou, et rien ne peut s'afficher avant qu'il ne
soit planté. Tout ce module raconte ce qui change quand le framework le plante avant toi, sur le
serveur.
Fais le compte : entre l'adresse tapée et le premier contenu utile, quatre attentes successives — le HTML, le JS, l'exécution, les données — dont chacune ne peut commencer que quand la précédente est finie. C'est une cascade (en anglais request waterfall) : chaque palier doit se remplir avant que l'eau ne coule au suivant.
En version tout-client, la page d'accueil de RendezVous ressemblait à ça : un
useState pour la liste des services, un pour chargement, un pour
erreur, un useEffect qui lance le fetch, et un
rendu qui commence par trois if (« si ça charge, un spinner ; s'il y a une
erreur, un message ; sinon, la liste »). Ce trio état / effet / triple-if, tu
l'as écrit cent fois en React Native : c'est le tribut normal de la vie « côté client »,
où l'on n'a pas les données au premier rendu et où il faut donc coder l'attente.
Ajoute deux ennuis moins visibles. Le poids du bundle d'abord — le bundle, c'est le gros paquet de JavaScript que le navigateur doit télécharger : tout ton code part chez l'utilisateur, y compris ce qui n'a aucune raison d'y être. Une bibliothèque de dates pèse quelques dizaines de kilo-octets, un client de base de données bien plus, et le téléphone télécharge, décompresse et compile tout ça avant d'afficher un seul mot.
Les secrets ensuite : puisque le code tourne dans le navigateur, il ne peut jamais contenir de clé d'API ni de mot de passe de base de données. Ce n'est pas une question de discipline, c'est physique — tout ce que le navigateur exécute, l'utilisateur peut le lire. Il faut donc obligatoirement une couche d'API intermédiaire, un serveur à écrire, déployer et sécuriser, dont le seul rôle est souvent de recopier une base de données vers du JSON.
Récapitulons les quatre douleurs, parce que les Server Components répondent exactement, une par une, à ces quatre-là : (1) un bundle JavaScript trop gros, (2) des données qui n'arrivent qu'après coup, (3) une cascade de spinners qui s'enchaînent, (4) l'impossibilité de toucher directement aux données sensibles.
Le module « HTTP & fetch » a posé le vocabulaire de l'aller-retour réseau : une requête, une réponse, un temps de latence incompressible. La cascade décrite ci-dessus n'est rien d'autre que plusieurs de ces allers-retours mis bout à bout, alors qu'ils auraient pu être faits en parallèle — ou, mieux, faits par une machine bien plus proche de la base de données. Retiens cette intuition : un aller-retour depuis le téléphone de l'utilisateur coûte cher ; un aller-retour depuis le serveur, qui est dans le même centre de données que la base, coûte presque rien. Déplacer le travail vers le serveur, ce n'est pas un caprice d'architecte, c'est de la géographie.
La révélation : « où ce code s'exécute-t-il ? »
Voici l'affirmation à graver, plus radicale qu'elle n'en a l'air : dans l'App
Router de Next.js, un composant s'exécute par défaut sur le serveur. Pas « peut
s'exécuter », pas « si tu configures quelque chose ». Tu crées app/page.tsx, tu
y écris un composant React tout ce qu'il y a de plus normal, et il tournera sur le
serveur ; son code ne sera jamais téléchargé par le navigateur.
C'est le retournement complet de ce que tu connais : en React Native, tu ne t'es jamais
demandé où tournait WorkoutCard, il n'y avait pas d'autre endroit. Le nom
officiel de ces composants qui tournent sur le serveur, c'est Server
Components (ou « RSC », React Server Components) ; ceux qui tournent —
aussi — dans le navigateur sont les Client Components.
Un restaurant, une cuisine et une salle. En cuisine (le serveur), on a le garde-manger, les fournisseurs, le coffre-fort aux recettes secrètes, et on fait tout le travail lourd : chercher, cuire, assembler. Ce qui en sort n'est ni la recette ni les ingrédients bruts, c'est un plat fini. En salle (le navigateur), on ne cuisine pas : on reçoit le plat, on le pose sur la table, et on gère ce qui est vivant — le client qui appelle, change d'avis, demande du sel. La salle réagit ; la cuisine produit.
La limite de l'analogie, tu la connais déjà : les composants de la salle passent quand même par la cuisine avant le service — « serveur seulement » contre « serveur puis client ». La section « 'use client' : ce que la directive fait vraiment » y revient en détail.
Concrètement : quand une requête arrive, Next.js exécute tes Server Components sur le serveur. Ils vont chercher leurs données, ils rendent leur JSX, et le résultat est sérialisé — converti en une suite d'octets qui peut voyager sur le réseau (voir la section « La frontière et les props sérialisables ») — dans un format compact appelé le RSC Payload. Ce payload n'est ni du HTML ni ton code : c'est une description de l'arbre rendu — le texte, la structure, et des marqueurs qui disent « ici, un Client Component, voici le fichier JavaScript à charger pour lui ». En parallèle, Next.js produit du HTML classique pour que le navigateur ait quelque chose à afficher immédiatement.
Reprends la page RendezVous. Le serveur reçoit la requête, exécute la page, qui interroge directement la base pour obtenir les services — quelques millisecondes, la base est à côté — rend le JSX avec les vraies données dedans, et envoie du HTML déjà rempli. L'utilisateur voit la liste dès la première image : pas de page blanche, pas de spinner, pas de cascade. Et le code qui interroge la base n'a jamais quitté le serveur : il ne pèse rien dans le bundle et ne révèle aucun secret.
// Aucune directive en haut du fichier : dans l'App Router,
// ce composant est donc un SERVER COMPONENT (le défaut).
import { db } from '@/lib/db';
// Remarque le mot-clé `async` : un composant serveur a le droit
// d'être asynchrone, parce qu'il n'est rendu qu'UNE fois, côté serveur.
export default async function PageAccueil() {
// Requête directe à la base. Les identifiants de connexion
// vivent dans les variables d'environnement du serveur et
// ne partiront JAMAIS vers le navigateur.
const services = await db.service.findMany({ where: { actif: true } });
// Quand ce JSX est produit, les données sont DÉJÀ dedans.
return (
<section>
<h1>Nos services</h1>
<ul>
{services.map((s) => (
<li key={s.id}>{s.nom} — {s.duree} min</li>
))}
</ul>
</section>
);
}
Compare avec la version « tout client » : plus de useState, de
useEffect, de spinner, de if (chargement). L'attente n'a pas
disparu — elle a été déplacée là où elle est courte et où l'utilisateur ne la subit pas
sous forme d'écran vide.
Oui, et ça surprend tout le monde. En React classique, un composant ne peut pas être
async : il est rappelé sans arrêt (module « Le modèle mental de React » du
guide Halterofit), et une fonction qui rend une promesse n'aurait aucun sens dans ce
cycle. Un Server Component, lui, n'est appelé qu'une seule fois, sur le serveur,
pour une requête donnée — pas de re-render, pas d'état qui évolue, pas de cycle
de vie. Dans ces conditions async redevient raisonnable : juste une fonction
qui met un peu de temps à retourner sa description.
La page affiche bien la liste des services. Mais tu ouvres la console du navigateur pour vérifier le nombre chargé : rien, pas une ligne. Le console.log s'est-il exécuté ?
// app/page.tsx — aucune directive
import { db } from '@/lib/db';
export default async function Page() {
const services = await db.service.findMany();
console.log('services chargés :', services.length);
return <ListeServices services={services} />;
}
Oui, il s'est exécuté — mais pas là où tu regardes. Ce composant tourne sur le serveur : son console.log est parti dans la sortie du terminal où tourne next dev, pas dans la console du navigateur. Le navigateur, lui, n'a jamais reçu ce code ; il a reçu le HTML qui en est sorti. C'est la conséquence la plus quotidienne du « où est-ce que ça tourne ? » : sur un Server Component, on débogue dans le terminal, et un composant marqué 'use client' loguera, lui, aux deux endroits — une fois côté serveur pour le HTML initial, une fois dans le navigateur à l'hydratation.
Ce qu'un Server Component peut et ne peut pas faire
Voici la partie où tout devient intuitif, à condition de ne jamais apprendre la liste par cœur. Chaque capacité et chaque interdiction découle d'un seul fait : le code tourne sur le serveur, avant que l'utilisateur n'ait vu quoi que ce soit. Si tu retiens ça, tu peux régénérer la liste toi-même à tout moment.
| Un Server Component… | Peut / Ne peut pas | Pourquoi, logiquement |
|---|---|---|
Être async et faire await |
✅ Peut | Il est rendu une seule fois, côté serveur. Rien ne l'empêche de prendre son temps. |
| Interroger directement une base de données | ✅ Peut | Le serveur est un environnement de confiance, et il est physiquement proche de la base. |
| Lire une clé d'API secrète | ✅ Peut | Ce code n'est jamais envoyé au navigateur : le secret reste secret. |
| Rendre d'autres Server Components | ✅ Peut | On reste du même côté de la frontière ; rien à traverser. |
| Rendre un Client Component | ✅ Peut | Il laisse un marqueur « ici, un composant client » que le navigateur remplira. |
Utiliser useState |
❌ Ne peut pas | L'état sert à changer dans le temps. Le composant a fini son travail et n'existe plus quand l'utilisateur regarde la page. Changer quoi, et pour qui ? |
Utiliser useEffect |
❌ Ne peut pas | Un effet se déclenche « après l'affichage ». Côté serveur, il n'y a pas d'affichage : il n'y a qu'une chaîne de caractères à produire. |
Poser un onClick / onChange |
❌ Ne peut pas | Un gestionnaire d'événement est une fonction. On ne peut pas envoyer une fonction dans du HTML ; et de toute façon, le serveur ne sera plus là pour l'exécuter. |
Toucher window, localStorage, navigator |
❌ Ne peut pas | Ces objets appartiennent au navigateur (module « Le navigateur »). Sur le serveur, ils n'existent tout simplement pas. |
Utiliser un contexte React (useContext) |
❌ Ne peut pas | Le contexte est un mécanisme de l'arbre React vivant, côté client. Un provider doit donc être un Client Component. |
| Utiliser un hook personnalisé qui utilise l'un des ci-dessus | ❌ Ne peut pas | Un hook n'est qu'une fonction : il hérite des contraintes de ce qu'il appelle. |
Regarde la colonne de droite : pas une seule interdiction arbitraire, que des
impossibilités physiques. Demander window sur un serveur,
c'est comme demander le numéro de plaque d'un vélo — ce n'est pas interdit, c'est vide de
sens.
Fais-toi l'exercice mental sur le cas le plus parlant, l'état. useState promet
deux choses : « garde-moi cette valeur d'un rendu à l'autre » et « rappelle ma fonction
quand elle change ». Or un Server Component n'a qu'un seul rendu — il produit son
HTML, il est jeté, la requête est terminée. Pas de « rendu suivant » où garder la valeur,
personne pour rappeler la fonction : le serveur est passé à la requête d'un autre
utilisateur. Les deux promesses sont vides ici ; c'est interdit non parce que quelqu'un l'a
décidé, mais parce que ça ne veut rien dire.
Même raisonnement pour onClick. Le HTML que le serveur envoie est du
texte : on n'écrit pas une fonction JavaScript dans du texte en espérant qu'elle
s'exécute — et même si on le pouvait, elle devrait s'exécuter deux secondes plus tard, dans
le navigateur, alors que le serveur a déjà oublié cette requête. La seule façon d'avoir un
vrai onClick, c'est que du JavaScript soit présent dans le
navigateur. D'où la section suivante.
Voici un fichier de RendezVous. Il ne fonctionne pas. Lis-le et dis pourquoi, en une phrase, sans regarder le corrigé.
'use client';
import { db } from '@/lib/db';
export default async function ListeServices() {
const services = await db.service.findMany();
return (
<ul>
{services.map((s) => <li key={s.id}>{s.nom}</li>)}
</ul>
);
}
Voir le corrigé
Le fichier se contredit lui-même. La première ligne,
'use client', annonce « ce composant vivra dans le navigateur ». Mais le
corps du composant fait deux choses réservées au serveur : il est async
et il interroge la base de données.
Sur le async : un composant client est rappelé sans arrêt
par React (props, état, parent — le trio du module « Le modèle mental de React »), et
React ne sait pas quoi faire d'une fonction-composant qui retourne une promesse dans ce
cycle. D'où l'erreur « async/await is not yet supported in Client Components, only
Server Components », dont le message souffle souvent la bonne piste : un
'use client' ajouté par erreur en haut d'un module écrit pour le serveur.
Sur la base de données : le problème le plus grave, même s'il est plus
discret. En marquant le fichier 'use client', tu demandes au bundler
d'embarquer ce fichier et tout ce qu'il importe dans le paquet envoyé au
navigateur : @/lib/db, son pilote, et potentiellement la chaîne de
connexion. Énorme et dangereux. En pratique Next.js t'arrêtera avant (et le paquet
server-only existe pour transformer ce genre d'erreur en échec de
compilation clair), mais l'intention du code est fausse dès le départ.
Le correctif tient en une touche : supprimer la ligne
'use client'. Ce composant n'a aucune interactivité — il affiche une
liste. Il n'a rien à faire côté client.
Le site refuse de démarrer : « createContext only works in Client Components. Add the "use client" directive at the top of the file to use it. » Pourquoi un contexte n'a-t-il aucun sens ici — et quel est le correctif qui ne fait pas basculer tout le site dans le navigateur ?
// app/layout.tsx — aucune directive
import { createContext } from 'react';
export const ThemeContext = createContext('clair');
export default function RootLayout({
children,
}: { children: React.ReactNode }) {
return (
<html lang="fr">
<body>
<ThemeContext.Provider value="sombre">
{children}
</ThemeContext.Provider>
</body>
</html>
);
}
Un contexte est un mécanisme de l'arbre React vivant : un fournisseur dépose une valeur, des consommateurs s'y abonnent, et quand elle change tout le monde est re-rendu. Or un Server Component n'a qu'un seul rendu — il produit sa description, la requête se termine, il est jeté : il n'y a plus d'arbre auquel s'abonner, ni de re-rendu à propager. L'abonnement n'est pas interdit, il est vide de sens, exactement comme useState. Le correctif n'est pas d'ajouter 'use client' en tête du layout racine : la directive est virale, et tout ce que le layout importe partirait avec lui. On extrait le fournisseur dans son propre petit fichier 'use client', qui ne fait que rendre {children}, et le layout serveur s'en sert comme d'un composant ordinaire.
'use client' : ce que la directive fait vraiment
Passons à l'autre côté. Puisque le défaut est « serveur », il faut un moyen de dire
« celui-là, non ». Ce moyen, c'est une ligne de texte placée tout en haut d'un fichier,
avant même les import :
'use client';
Une chaîne de caractères seule sur une ligne. Pas un import, pas un appel de fonction : une directive, un marqueur que l'outil de compilation lit avant tout le reste.
Voici la nuance la plus mal comprise de tout l'écosystème :
'use client' ne veut PAS dire « ce composant ne s'exécute que dans le
navigateur ». Sa première exécution a lieu côté serveur, pour produire le HTML
initial que l'utilisateur verra avant même que le JavaScript n'ait fini de se charger ;
ensuite seulement il est ré-exécuté dans le navigateur pour devenir interactif — c'est
l'hydratation (React « rebranche » les gestionnaires d'événements sur le
HTML déjà présent). Ce que la directive fait vraiment, c'est marquer une
frontière : « à partir d'ici, embarque ce fichier dans le bundle client ». Le
vocabulaire officiel dit qu'elle déclare une boundary entre deux graphes de
modules.
Reprends l'opposition posée en ouverture, elle se décline ainsi :
- Server Component = s'exécute uniquement sur le serveur. Son code n'est jamais envoyé au navigateur. Il ne peut donc pas être interactif.
- Client Component = s'exécute une fois sur le serveur (pour le HTML initial), puis dans le navigateur (pour l'interactivité). Son code voyage jusqu'à l'utilisateur.
Vu comme ça, le mot « client » dans 'use client' se lit correctement : il
ne dit pas où ça tourne, il dit où ça finit. C'est une étiquette
d'expédition, pas une adresse d'exécution.
« Un Client Component s'exécute deux fois » n'est pas une subtilité théorique : c'est la cause de l'erreur que tu rencontreras en premier sur un vrai projet Next.js. Elle ressemble à ça :
Hydration failed because the server rendered HTML
didn't match the client.
Traduction : le HTML fabriqué par le serveur et celui que le navigateur vient de recalculer ne sont pas identiques, et React refuse de rebrancher ses gestionnaires sur un arbre qu'il ne reconnaît pas. La cause est presque toujours la même — une valeur qui ne peut pas donner le même résultat aux deux exécutions :
'use client';
export function Header() {
// ⛔ Exécuté sur le serveur à 14:03:07, puis dans le navigateur à 14:03:09.
// Deux HTML différents → erreur d'hydratation.
const now = new Date().toLocaleTimeString();
return <p>Il est {now}</p>;
}
Même mécanisme avec Math.random(), un identifiant tiré au hasard, ou une
valeur lue dans localStorage (qui n'existe pas côté serveur — c'est le
piège vu dans le module
Le navigateur : DOM, événements, rendu). La parade est
toujours l'une des deux : soit la valeur variable est calculée
uniquement dans le navigateur, dans un useEffect qui ne
s'exécute jamais sur le serveur ; soit elle est décidée par le serveur
et descendue en prop, et alors les deux exécutions reçoivent la même.
Le développeur a lu la parade et a bien déplacé la mise à jour de l'heure dans un useEffect. Pourtant « Hydration failed because the server rendered HTML didn't match the client » revient à chaque chargement. Où reste l'écart ?
'use client';
import { useState, useEffect } from 'react';
export function Horloge() {
const [heure, setHeure] = useState(new Date().toLocaleTimeString());
useEffect(() => {
const id = setInterval(
() => setHeure(new Date().toLocaleTimeString()),
1000,
);
return () => clearInterval(id);
}, []);
return <p>Il est {heure}</p>;
}
L'effet corrige le battement de l'horloge, pas son premier affichage. La valeur initiale de useState est calculée pendant le rendu — et ce rendu a lieu deux fois : une fois sur le serveur pour produire le HTML, une fois dans le navigateur à l'hydratation, à deux instants différents. Les deux HTML diffèrent d'une seconde ou deux, et React refuse de rebrancher. Un useEffect ne s'exécute jamais sur le serveur, donc il ne peut rien corriger avant la comparaison : il ne s'exécute qu'après. Correctif : partir d'une valeur neutre que les deux exécutions produisent à l'identique — useState(null), on n'affiche rien (ou un tiret) tant qu'elle est nulle — et laisser l'effet poser la vraie heure juste après. Règle générale : tout ce qui est calculé pendant le rendu d'un Client Component doit être reproductible à l'identique aux deux endroits.
Deuxième propriété capitale : la directive est virale vers le bas. Une
fois qu'un fichier est marqué 'use client', tous les modules qu'il
importe rejoignent le bundle client avec lui, ainsi que les composants qu'il rend
directement ; inutile de répéter la directive dans chaque enfant, elle se propage le long
des import. Très pratique (une seule ligne, à l'entrée de la zone interactive)
et très dangereux si tu la places trop haut : marque-la en haut de ta page principale et
tout ce que cette page importe — carte de service, pied de page, formatteur de dates,
bibliothèque de validation — bascule dans le navigateur. Tu as réinventé l'application
« tout client » de la section « Le problème que ça résout : quand tout partait dans le
navigateur », avec en prime la complexité de Next.js.
Le symptôme est innocent : un composant refuse un onClick, tu ajoutes
'use client' en haut du fichier, l'erreur disparaît. Puis tu recommences un
cran plus haut, et un jour la directive est en tête de la page racine.
Le signe qui doit t'arrêter : tu écris la directive en tête d'un fichier de plus de cent cinquante lignes. À ce moment-là, ce n'est jamais de basculer qu'il s'agit, c'est de découper — et la section « Le patron de composition : les Client Components aux feuilles » dit comment.
children
La viralité suit les imports, pas le rendu visuel. Si un Client
Component reçoit un Server Component via children (ou n'importe quelle
autre prop), ce Server Component ne rejoint pas le bundle client : il n'a
jamais été importé par le fichier client, il a été rendu sur le serveur et passé
déjà cuit. C'est exactement ce qui rend possible le patron de composition de la
section « Le patron de composition : les Client Components aux feuilles ». Garde cette phrase : la frontière suit les imports, pas
l'imbrication visuelle.
La frontière client
'use client';
import { useState } from 'react';
export default function AppointmentForm({ services }) {
const [service, setService] = useState('');
// … interactivité ici
}
La directive marque le début du monde navigateur : à partir de cette ligne, on a le droit d'utiliser l'état, les événements et les API du navigateur. Et tout ce que ce fichier importe le rejoint automatiquement dans le bundle client — d'où la règle : place-la le plus bas possible dans l'arbre.
Rien de ce que tu as appris sur React ne devient faux ici. Relis le module Le modèle mental de React du guide Halterofit : UI = fonction(état), le JSX qui n'est qu'une description en mémoire, les deux phases render puis commit, la réconciliation qui compare l'ancien arbre au nouveau. Tout ça fonctionne exactement pareil dans un Server Component. Un Server Component est toujours une fonction qui reçoit des données et retourne une description d'écran ; c'est simplement que le calcul se fait dans une autre machine, et que la description est expédiée par le réseau (le RSC Payload) au lieu de rester en mémoire. Le moteur n'a pas changé. Il a juste déménagé une partie de l'atelier.
La frontière et les props sérialisables
Dès qu'il y a deux machines, il y a un réseau entre elles, et dès qu'il y a un réseau, il
faut convertir les données en quelque chose qui voyage. C'est ce qu'on
appelle la sérialisation : transformer une valeur vivante en mémoire en une suite
d'octets, puis la reconstruire de l'autre côté. Tu connais déjà le principe par
JSON.stringify et le module « HTTP & fetch ».
D'où la règle : toute prop qui traverse la frontière serveur → client doit être sérialisable. Ce n'est pas une préférence stylistique, c'est une contrainte de transport. Une valeur qui ne peut pas être décrite en octets ne peut pas être remise au navigateur, point.
| Ce qui traverse ✅ | Ce qui ne traverse pas ❌ |
|---|---|
Chaînes, nombres, booléens, null, undefined, bigint |
Les fonctions ordinaires (une fonction n'est pas de la donnée : c'est du code, et le serveur ne sera plus là pour l'exécuter) |
Objets simples (créés avec { }) et tableaux, s'ils ne contiennent eux-mêmes que des valeurs sérialisables |
Les instances de classes que tu as écrites toi-même — l'objet perdrait ses méthodes en route |
Les Date — oui, vraiment : React sait les transporter et les reconstruire de l'autre côté |
Les objets à prototype null |
Map, Set, TypedArray, ArrayBuffer |
Les Symbol locaux, créés avec Symbol('x') |
| Du JSX — un élément React est une valeur, donc il voyage (c'est la clé de la section « Le patron de composition : les Client Components aux feuilles ») | Tout ce qui contient, même en profondeur, un des cas ci-dessus |
| Les promesses — on verra ce cas particulier à la section « Passer une promesse au client : use() et Suspense » | |
Les Server Actions, c'est-à-dire les fonctions marquées 'use server' — la seule exception à l'interdiction des fonctions (module « Route Handlers & Server Actions ») |
|
Cas particulier : les Symbol enregistrés globalement (Symbol.for('x')) traversent, parce que le nom suffit à les retrouver de l'autre côté. |
|
La ligne la plus utile du tableau est la première de la colonne de droite — les fonctions — parce que c'est celle sur laquelle tu vas te cogner. Tu voudras écrire ce que tu as toujours écrit en React : passer un callback du parent vers l'enfant (les données descendent, les événements remontent — module « Le modèle mental de React »). Mais si le parent est un Server Component et l'enfant un Client Component, ce callback devrait littéralement traverser Internet. Impossible : on ne met pas du code exécutable dans un flux de données, et de toute façon le serveur qui a créé cette fonction a terminé son travail depuis longtemps.
// PAGE SERVEUR (pas de 'use client' ici)
export default async function Page() {
const services = await db.service.findMany();
// ❌ `onSelect` est une FONCTION. Elle ne peut pas
// franchir la frontière serveur → client.
return <ServiceSelector
services={services}
onSelect={(id) => console.log(id)}
/>;
}
services passe sans problème (c'est un tableau d'objets simples).
onSelect, lui, bloque tout.
Le message d'erreur que tu verras alors ressemble à ceci — et maintenant tu peux le lire en entier au lieu de le subir :
Error: Functions cannot be passed directly to Client Components
unless you explicitly expose it by marking it with "use server".
Traduction : « une fonction ne traverse pas, sauf si c'est une Server Action. »
Décortique-le mot à mot, c'est un excellent entraînement. « Functions cannot be passed
directly to Client Components » : on te dit quoi (une fonction) et
où (vers un Client Component). « unless you explicitly expose it by
marking it with "use server" » : on te donne la seule échappatoire, les Server Actions,
que le module « Route Handlers & Server Actions » détaille. Ce que le navigateur reçoit
alors n'est pas la fonction mais une référence, une adresse à rappeler ; le code
reste au chaud sur le serveur. Il existe une variante encore plus fréquente de ce message,
quand tu écris directement un onClick dans du JSX de Server Component :
Error: Event handlers cannot be passed to Client Component props.
<button onClick={function onClick}>
If you need interactivity, consider converting part of this
to a Client Component.
Même cause, autre habillage. La dernière phrase te souffle même la solution : découpe, et rends cette partie-là cliente.
Et la bonne nouvelle du tableau : les Date traversent très bien.
Beaucoup de tutoriels datés racontent qu'il faut les convertir en chaînes avant de les
passer, parce qu'ils raisonnent avec JSON.stringify en tête ; or le format de
sérialisation de React est plus riche que JSON — il gère nativement Date,
Map, Set, les bigint et les tableaux typés. Ce qui
casse vraiment, ce sont les instances de classes maison, un objet d'ORM avec des
méthodes dessus par exemple : les données passeraient, les méthodes disparaîtraient, et
React refuse plutôt que de te livrer un objet mutilé.
Voici quatre composants de RendezVous et la façon dont ils s'imbriquent. Un seul fichier porte la directive. Question : lesquels de ces quatre composants finiront dans le bundle JavaScript téléchargé par le navigateur ?
// app/page.tsx — pas de directive
import EnTete from './en-tete'; // pas de directive
import PanneauReservation from './panneau'; // 'use client'
import ResumeClinique from './resume'; // pas de directive
export default async function Page() {
return (
<>
<EnTete />
<PanneauReservation>
<ResumeClinique />
</PanneauReservation>
</>
);
}
Voir le corrigé
Un seul : PanneauReservation. Chacun des quatre illustre
une règle différente.
Page et EnTete — pas de
directive, donc Server Components ; ils s'exécutent sur le serveur, produisent leur
description, et leur code ne part jamais (Page a même le droit d'être
async). Bundle client : non.
PanneauReservation porte 'use client' : il
part dans le bundle, ainsi que tous les modules qu'il importe.
Bundle client : oui.
ResumeClinique — c'est le piège, et la question à
laquelle il faut savoir répondre en entrevue. Visuellement, il est à
l'intérieur d'un composant client. Mais il n'est pas importé par le
fichier client : c'est page.tsx, fichier serveur, qui l'importe et le
passe via children. Il est donc rendu sur le serveur, et seul son
résultat est glissé dans le trou laissé par le panneau. Bundle client :
non. La règle : la frontière suit la ligne import,
pas l'imbrication visuelle du JSX — c'est ce qui rend possible le patron de la section
suivante.
Lesquelles de ces props peuvent traverser la frontière serveur → client : un tableau d'objets, une Date, une fonction fléchée, une instance de classe ?
Le tableau d'objets simples ✅ et la Date ✅ (le format de sérialisation de React est plus riche que JSON : il gère aussi Map, Set, bigint, les tableaux typés, le JSX et les promesses). La fonction fléchée ❌ — sauf si c'est une Server Action marquée 'use server', seule exception. L'instance de classe ❌ : ses données passeraient mais ses méthodes disparaîtraient, donc React refuse plutôt que de livrer un objet mutilé.
Le patron de composition : les Client Components aux feuilles
On arrive au patron que tu dois savoir dessiner au tableau sans hésiter. Il tient en une image : un arbre majoritairement serveur, avec de petites îles clientes tout en bas, aux feuilles. Pas une grosse zone cliente en haut. Des petits îlots interactifs, aussi bas et aussi petits que possible.
Pourquoi « en bas » ? Parce que la viralité de 'use client' descend : plus tu
poses la frontière haut, plus la zone qu'elle capture est grande et plus tu expédies de
JavaScript inutile au navigateur. La question à te poser devant chaque composant est donc
simple — « celui-ci a-t-il vraiment besoin de mémoire ou de réagir à un clic ? ».
Si la réponse est non, et c'est non la plupart du temps, laisse-le serveur. Appliquons à
RendezVous : la page affiche la liste des services (données, aucune interactivité) et un
formulaire de réservation (état, validation, clics). Le découpage s'impose de lui-même.
// Aucune directive : SERVER COMPONENT.
import { db } from '@/lib/db';
import AppointmentForm from './formulaire-rendez-vous';
export default async function PageRendezVous() {
// Les données sont récupérées AVANT le premier affichage,
// directement depuis la base, sans passer par une API.
const services = await db.service.findMany({ where: { actif: true } });
return (
<main>
<h1>Prendre rendez-vous</h1>
{/* Cette liste n'a aucune interactivité : elle reste
côté serveur, et son code ne part pas au navigateur. */}
<ul>
{services.map((s) => (
<li key={s.id}>{s.nom} — {s.duree} min</li>
))}
</ul>
{/* Les données traversent la frontière en tant que
props sérialisables (tableau d'objets simples). */}
<AppointmentForm services={services} />
</main>
);
}
La page fait une chose : aller chercher les données et les distribuer. Elle ne gère aucun état.
'use client'; // ← ici commence le monde du navigateur
import { useState } from 'react';
// Le formulaire reçoit les services DÉJÀ chargés : il n'a
// aucun fetch à faire, aucun spinner à afficher au démarrage.
export default function AppointmentForm({ services }) {
const [serviceId, setServiceId] = useState('');
const [nom, setNom] = useState('');
return (
<form>
<select value={serviceId} onChange={(e) => setServiceId(e.target.value)}>
<option value="">Choisir un service</option>
{services.map((s) => (
<option key={s.id} value={s.id}>{s.nom}</option>
))}
</select>
<input value={nom} onChange={(e) => setNom(e.target.value)} />
<button type="submit">Réserver</button>
</form>
);
}
Un champ contrôlé, exactement comme dans le module « Formulaires 1 — contrôlé ou non ». Rien de nouveau côté React : ce qui est nouveau, c'est que ce fichier est le seul des deux à voyager jusqu'au navigateur.
Compte ce qu'on a gagné par rapport à la version « tout client » de la section
« Le problème que ça résout : quand tout partait dans le navigateur ».
(1) La liste des services est dans le HTML de la première réponse : plus
de page blanche ni de spinner initial. (2) Le client de base de données
ne pèse rien dans le bundle, il n'est jamais parti. (3) Plus d'API
intermédiaire à écrire juste pour recopier la table service en JSON.
(4) Le formulaire garde exactement son interactivité ; ce qu'il a perdu,
c'est du code d'attente — plus de useEffect de chargement, plus de
if (chargement) return <Spinner />. Il naît avec ses données.
Quand l'îlot doit contenir du contenu serveur : le trou children
Un cas revient sans arrêt et déroute au début. RendezVous affiche les détails d'un service dans une fenêtre modale : la fenêtre a besoin d'état (ouverte / fermée, animation, fermeture au clic extérieur), c'est forcément un Client Component ; mais son contenu — la fiche tirée de la base — n'a besoin d'aucune interactivité et gagnerait à rester serveur. Comment mettre du serveur dans du client ?
Réponse : on ne l'importe pas, on le fait descendre. Le composant client
ouvre un trou avec children, et c'est le parent serveur qui le remplit. Tu as
déjà vu ce mécanisme dans le module « Le modèle mental de React » avec le composant
Card : un contenant qui ne sait rien de son contenu. Ici, la même idée devient
une technique d'architecture.
// modale.tsx — CLIENT : il gère l'état d'ouverture,
// mais ne sait absolument RIEN de son contenu.
'use client';
import { useState } from 'react';
export default function Modal({ children }) {
const [ouverte, setOuverte] = useState(false);
return (
<>
<button onClick={() => setOuverte(true)}>Détails</button>
{ouverte && <div className="modale">{children}</div>}
</>
);
}
// page.tsx — SERVEUR : il remplit le trou de la modale
// avec un composant serveur qui, lui, touche la base.
import Modal from './modale';
import FicheService from './fiche-service'; // serveur, async
export default function Page() {
return (
<Modal>
<FicheService id="physio" />
</Modal>
);
}
FicheService est rendu sur le serveur, puis son résultat déjà
calculé est glissé dans children. Son code ne rejoint jamais le bundle client
— parce que le fichier modale.tsx ne l'importe pas.
page.tsx est le
propriétaire des deux : c'est lui qui écrit leur JSX, donc c'est lui qui les
fait naître, chacun du bon côté. En bas, la modale n'est plus seulement le parent
visuel de la fiche, elle en devient le propriétaire — et la viralité de la directive
l'entraîne, avec le client de base de données derrière. D'où la phrase à retenir :
la frontière suit les imports, pas l'imbrication visuelle.
C'est le patron d'entrevue. À la question « comment imbriquer un Server Component
dans un Client Component ? », la réponse tient en une phrase : on ne l'importe pas
depuis le client, on le passe en children (ou en prop) depuis un parent
serveur — parce que la frontière suit les imports et que le JSX est une valeur
sérialisable, donc il traverse. Le même patron résout le cas des providers de
contexte : le contexte étant un mécanisme client, un provider doit être un Client
Component qui rend {children}, et on l'utilise dans le layout serveur en
enveloppant les enfants. Les enfants restent serveur, le provider reste client.
Cette question n'est pas nouvelle pour toi, même si tu ne l'avais jamais formulée ainsi. Relis le module Supabase : backend et synchronisation du guide Halterofit. Il y a du code qui tourne sur l'appareil (les composants React Native, la base locale, la logique d'affichage) et du code qui tourne sur l'infrastructure Supabase (les Edge Functions, les politiques RLS, les triggers SQL). Et tu sais déjà, d'instinct, ce qui ne se mélange pas : on ne met pas une clé de service dans l'app mobile, parce qu'on peut décompiler un APK ; on ne manipule pas l'écran depuis une Edge Function, parce qu'elle n'a pas d'écran.
Server et Client Components appliquent exactement ce raisonnement, avec une
différence de forme qui est aussi ce qui le rend déroutant : les deux mondes sont
maintenant écrits dans le même langage, avec la même syntaxe, dans des fichiers
voisins. Une Edge Function, tu ne peux pas la confondre avec un composant : le
dossier est différent, le fichier est différent, tout crie « je suis ailleurs ». Ici,
deux fichiers .tsx côte à côte peuvent tourner dans deux machines à des
milliers de kilomètres l'une de l'autre. D'où la nécessité d'un marqueur explicite —
'use client' — et d'une habitude de lecture, celle de la section
« Comment lire un fichier Next.js et savoir où il tourne ».
À l'écran, cette modale rend exactement le même arbre que la version en children vue plus haut : un composant client qui contient une fiche serveur. Pourtant celle-ci ne compile pas, et embarquerait @/lib/db dans le navigateur. Qu'est-ce qui change ?
// modale.tsx
'use client';
import { useState } from 'react';
import FicheService from './fiche-service'; // serveur, async, touche la base
export default function Modal({ id }: { id: string }) {
const [ouverte, setOuverte] = useState(false);
return (
<>
<button onClick={() => setOuverte(true)}>Détails</button>
{ouverte && <div className="modale"><FicheService id={id} /></div>}
</>
);
}
La ligne import FicheService from './fiche-service'. Ici, c'est le fichier client qui importe la fiche : la viralité de 'use client' suit les import, donc FicheService — et @/lib/db derrière lui — rejoint le bundle du navigateur. Et comme un composant client ne peut pas être async, la fiche ne peut de toute façon pas s'exécuter là. Dans la version qui marche, le fichier client ne connaît que children : c'est le parent serveur qui importe les deux, rend la fiche sur le serveur, et glisse son résultat déjà cuit dans le trou. Deux arbres identiques à l'œil, deux graphes d'imports opposés — d'où la phrase à retenir : la frontière suit les imports, pas l'imbrication visuelle.
Passer une promesse au client : use() et Suspense
Un dernier scénario, pour que tu saches qu'il existe. Sur la page RendezVous, la liste des
services se charge en 20 ms, mais les créneaux disponibles demandent 800 ms à un système
de réservation externe : faire await sur les deux dans la page prendrait le contenu
rapide en otage. React 19 offre une sortie élégante — un Server Component a le droit de
lancer une requête sans l'attendre et de passer la promesse elle-même à un
Client Component (elles sont sérialisables, revois le tableau de la section « La frontière et
les props sérialisables »). Le composant client la déballe avec le hook use(),
pendant qu'une frontière <Suspense> affiche un état d'attente à sa place.
La démonstration complète — le code des deux fichiers, le streaming qui va avec, et le
cousin cache() de React qui évite de recharger la même donnée dans trois composants
serveur — vit dans son contexte naturel : le module
Rendu & data fetching.
Comment lire un fichier Next.js et savoir où il tourne
Voici l'outil pratique du module — celui que tu utiliseras vraiment, en entrevue technique comme au premier jour d'un stage. Quatre coups d'œil, dans cet ordre, et tu sais où tourne le fichier.
| # | Ce que tu cherches | Ce que ça t'apprend |
|---|---|---|
| 1 | La toute première ligne : y a-t-il 'use client' ? |
C'est la réponse la plus rapide et la plus fiable. Si oui → composant client, il part dans le bundle. Si non → serveur par défaut. Ce seul réflexe répond à 90 % des cas. |
| 2 | Le mot-clé async devant le composant |
Un composant async ne peut être que serveur. C'est une confirmation, et un signal fort : ce fichier va chercher des données. |
| 3 | useState, useEffect, onClick, window |
N'importe lequel de ces mots impose le côté client. Si tu les vois sans 'use client' en tête, tu viens de trouver un bug (ou une directive héritée d'un fichier parent — vérifie qui importe qui). |
| 4 | Les import en haut : importe-t-il un composant client ? |
Le fichier peut rester serveur tout en rendant des îlots clients. C'est le cas normal et sain d'une page. L'inverse — un fichier client qui importe un module serveur — est un problème. |
Une précision sur le point 3, parce que c'est là qu'on se fait avoir : un fichier
sans 'use client' n'est pas forcément un Server Component — si le seul
fichier qui l'importe est marqué client, il hérite de la frontière par viralité. Quand un
doute subsiste, la question n'est plus « qu'y a-t-il dans ce fichier ? » mais « qui
importe ce fichier ? » : remonte la chaîne d'imports jusqu'à une directive, ou
jusqu'à un fichier de app/.
Applique cette grille à un vrai projet et tu verras toujours la même forme : une majorité
de fichiers sans directive, qui chargent et affichent ; une poignée de fichiers
'use client' aux noms révélateurs — formulaire,
modale, menu-deroulant, carrousel. Un projet qui a
'use client' partout n'est pas une fatalité technique, c'est un signe de
découpage bâclé — et tu peux le dire, poliment, en entrevue.
Applique la checklist à ce fichier de RendezVous. Où tourne-t-il, quelle erreur va-t-il produire, et pourquoi ?
import { db } from '@/lib/db';
export default async function CarteService({ id }) {
const service = await db.service.findUnique({ where: { id } });
return (
<article>
<h2>{service.nom}</h2>
<button onClick={() => alert(service.description)}>
En savoir plus
</button>
</article>
);
}
Voir le corrigé
Coup d'œil 1 : pas de 'use client' → Server Component.
Coup d'œil 2 : async + requête base de données → ça
confirme, et c'est parfaitement légitime jusqu'ici.
Coup d'œil 3 : un onClick. Contradiction.
L'erreur ressemblera à : « Event handlers cannot be passed to Client Component props », avec la ligne fautive et la suggestion « If you need interactivity, consider converting part of this to a Client Component ».
Pourquoi — et c'est ça qu'il faut savoir expliquer, pas juste
reconnaître. Ce composant s'exécute sur le serveur et n'y produit qu'une
description à sérialiser pour traverser le réseau ; une fonction fléchée n'est
pas de la donnée, c'est du code. Et même en supposant qu'on puisse l'expédier, elle
devrait s'exécuter plus tard, dans le navigateur, alors que le serveur a terminé sa
requête et oublié son contexte : la variable service qu'elle capture
n'existerait plus nulle part. L'interdiction est une conséquence, pas une règle de
Next.js.
Le correctif idiomatique n'est pas d'ajouter
'use client' en haut du fichier — ça casserait la requête et ferait voyager
@/lib/db. C'est de découper : CarteService reste serveur pour
la donnée et le titre, et le bouton part dans un tout petit fichier client à qui on
passe la description en prop (une chaîne : parfaitement sérialisable). Le patron
« Client Components aux feuilles », appliqué à la lettre.
Ce que ça change pour ta tête de développeur mobile
Terminons par le déplacement mental, puisque c'est l'objet réel de ce module. En React Native, tu vis dans un monde mono-lieu : tout ton JavaScript tourne sur l'appareil — composants, hooks, base locale, logique métier, calcul de l'e1RM. Le serveur, quand il y en a un, est une entité extérieure à qui tu parles par HTTP, jamais quelque chose qui exécute ton code React. Confortable : aucune question à se poser, faute d'alternative.
Next.js te fait passer à un monde bi-lieu, et le coût d'entrée est
exactement là. Ce n'est pas de la syntaxe nouvelle — tout le code de ce module est du React
que tu sais déjà lire — c'est une dimension supplémentaire à garder en tête en
permanence. Comme le passage du synchrone à l'asynchrone : la syntaxe de await
s'apprend en dix minutes, mais raisonner en « ce code ne s'exécute pas maintenant » prend
des semaines. Ici, c'est « ce code ne s'exécute pas ici ».
| Réflexe | React Native (mono-lieu) | Next.js App Router (bi-lieu) |
|---|---|---|
| Première question devant un fichier | « Qu'est-ce que ça affiche ? » | « Où est-ce que ça tourne ? » puis « qu'est-ce que ça affiche ? » |
| Aller chercher des données | useEffect + fetch + état de chargement | await directement dans le composant serveur |
| Secrets et clés d'API | Jamais dans l'app — un backend est obligatoire | Dans un Server Component, sans intermédiaire |
| État et interactivité | Partout, sans y penser | Uniquement dans les fichiers 'use client' |
| Passer un callback à un enfant | Toujours possible | Impossible à travers la frontière serveur → client |
| Coût d'un composant en plus | Un peu de bundle, toujours | Zéro bundle s'il reste serveur |
Une chose ne change pas, et c'est rassurant : ton React reste ton React.
Les props descendent, les événements remontent, le JSX est une description, la
réconciliation compare deux arbres, les key suivent l'identité des éléments —
tout ton investissement dans le modèle mental de React est intégralement transférable. Tu
ajoutes simplement une carte à deux territoires par-dessus le moteur que tu connais.
Et il y a un bénéfice inattendu pour toi, parce que tu viens du mobile : l'obsession pour la taille du bundle et le temps avant le premier affichage utile, tu la comprends déjà d'instinct — tu sais ce que coûte un téléphone bas de gamme sur un réseau moyen. Beaucoup de développeurs web de bureau, avec leur fibre, ne le sentent pas. C'est un angle à assumer en entrevue.
« C'est quoi, un Server Component ? »
« C'est un composant React qui s'exécute uniquement sur le serveur.
Dans l'App Router de Next.js, c'est le comportement par défaut : tout composant est
serveur tant qu'on n'a pas écrit 'use client'. Concrètement, il peut être
async, faire un await fetch ou interroger une base de
données directement, et utiliser des clés secrètes — puisque son code n'est jamais
envoyé au navigateur. En contrepartie, il ne peut pas avoir d'état, d'effets, de
gestionnaires d'événements ni toucher aux API du navigateur, parce qu'il a fini son
travail avant même que l'utilisateur ne voie la page. Le gain principal, c'est
double : les données sont là dès le premier affichage, et ce code pèse zéro octet
dans le bundle client. »
« Que fait exactement la directive 'use client' ? »
« Elle marque une frontière, pas un lieu d'exécution — et c'est la
nuance que beaucoup ratent. Un Client Component est quand même pré-rendu sur le
serveur pour produire le HTML initial ; il est ensuite hydraté dans le navigateur pour
devenir interactif. Donc 'use client' ne veut pas dire "seulement dans le
navigateur", ça veut dire "ce fichier part aussi dans le navigateur".
Et elle est virale vers le bas : tous les modules que ce fichier
importe rejoignent le bundle client avec lui. D'où la règle pratique : on la place le
plus bas possible dans l'arbre, sur les petits composants vraiment interactifs, pas en
haut d'une page. Une exception importante : ce qu'on reçoit en children
n'est pas concerné, parce que la viralité suit les imports, pas l'imbrication
visuelle. »
« Comment mets-tu de l'interactivité dans une page rendue au serveur ? »
« J'extrais la partie interactive dans un composant séparé marqué
'use client', et je le garde le plus près possible des feuilles de
l'arbre. La page reste un Server Component : elle va chercher les données, puis elle
les passe en props à l'îlot client — en s'assurant que ces props sont
sérialisables : des chaînes, des nombres, des objets simples, des
dates ; jamais des fonctions ni des instances de classes.
Si l'îlot client doit contenir du contenu serveur — typiquement une modale
cliente autour d'une fiche qui vient de la base — je ne l'importe pas depuis le
fichier client : je le passe en children depuis le parent serveur. Le
contenu est alors rendu sur le serveur et seul son résultat est glissé dans le trou.
Et si j'ai besoin de renvoyer une action au serveur au clic, je passe une Server
Action, la seule catégorie de fonctions autorisée à traverser la frontière. »
Dans l'App Router, un composant est Server Component par défaut : il
s'exécute sur le serveur, peut être async, interroger une base de données
et lire des secrets, et son code ne part jamais au navigateur. En échange, il n'a ni
useState, ni useEffect, ni onClick, ni
window — non par décret, mais parce que ces choses n'ont pas de sens dans
un lieu qui a fini son travail avant que l'utilisateur ne regarde.
'use client' marque une frontière, pas un lieu : le
composant est aussi pré-rendu sur le serveur puis hydraté, et la directive est
virale vers le bas le long des imports. Les props qui traversent doivent
être sérialisables (chaînes, nombres, objets simples, dates, Map/Set,
JSX, promesses — mais pas les fonctions, sauf Server Actions, ni les instances de
classes). Le patron canonique : garder les Client Components aux feuilles,
charger les données en haut côté serveur, et faire descendre le contenu serveur dans les
îlots clients via children. Et devant chaque fichier, une seule question à
se poser en premier : « où est-ce que ça tourne ? »
Et ailleurs : le raisonnement « où ce code s'exécute-t-il, et
qu'est-ce que ce lieu me permet ? » dépasse largement Next.js — c'est la question
d'architecture des systèmes distribués, et tu la retrouveras dans les Edge Functions
Supabase de ton projet mobile, les Cloud Functions, les workers Cloudflare, ou la question
« est-ce que je calcule ça sur l'appareil ou dans le nuage ? ». Le modèle « un défaut
serveur, une frontière explicite, des îlots interactifs » n'est pas propre à Next.js non
plus : Remix, Astro (ses islands et sa
directive client:load), SvelteKit et Nuxt
reposent sur des variantes de la même idée. Comprendre les Server Components une fois, en
profondeur, te rend lisible tout ce pan de l'écosystème frontend moderne.
- Le défaut de l'App Router
- Server Component, sans rien écrire. C'est
'use client'qui fait exception, jamais l'inverse - Les quatre ❌ d'un Server Component
useState,useEffect, les gestionnaires (onClick…), etwindow/localStorage—useContextavec euxasync- autorisé sur un Server Component (rendu une seule fois) ; interdit sur un Client Component
- Le
console.logd'un composant serveur - sort dans le terminal de
next dev, pas dans la console du navigateur - Ce qui traverse la frontière
- chaînes, nombres, booléens, objets simples, tableaux,
Date,Map,Set, JSX, promesses, Server Actions - Ce qui ne traverse pas
- les fonctions ordinaires et les instances de classes — et tout objet qui en contient, même en profondeur
- Où se met la directive
- tout en haut du fichier, avant les
import— c'est une directive, pas une instruction 'use server'- ne déclare pas un Server Component — c'est l'absence de directive qui le fait. Il marque une Server Action