
React Server Components : ce qui change vraiment
Depuis l'arrivée de l'App Router de Next.js, les React Server Components (RSC) sont devenus le modèle par défaut. Beaucoup d'équipes les utilisent sans vraiment savoir ce qui se passe sous le capot, et finissent par ajouter "use client" partout « pour que ça marche ». Résultat : on perd l'essentiel des bénéfices.
Dans cet article, on décortique le modèle : ce qu'est un Server Component, où passe la frontière avec le client, et les règles simples qui permettent d'en tirer parti.
Le problème que les RSC résolvent
Dans une application React classique (SPA ou SSR « à l'ancienne »), tout le code des composants est envoyé au navigateur, même celui qui ne sert qu'à afficher des données statiques. Une page produit qui formate du markdown ou des dates embarque la librairie de markdown et la librairie de dates dans le bundle JavaScript, alors que l'utilisateur n'interagit jamais avec.
Autre problème : le chargement des données. On affiche la page, puis un useEffect déclenche un appel API, puis un composant enfant déclenche un autre appel… C'est la fameuse cascade de requêtes (waterfall).
Server Component vs Client Component
Un Server Component s'exécute uniquement sur le serveur (au build ou à la requête). Son code n'est jamais envoyé au navigateur : seul le résultat rendu l'est. Il peut être async, lire une base de données, accéder à des secrets, et importer de grosses librairies sans impact sur le bundle.
Un Client Component est un composant React « classique » : il est rendu sur le serveur pour le HTML initial, puis hydraté dans le navigateur. C'est le seul endroit où l'on peut utiliser useState, useEffect, des gestionnaires d'événements ou les API du navigateur.
La directive "use client" est une frontière, pas une étiquette
C'est le point le plus mal compris. "use client" ne marque pas seulement un composant : elle marque un point d'entrée. Tout ce que ce fichier importe devient aussi du code client. Placer la directive tout en haut de l'arbre revient donc à transformer toute l'application en Client Components.
La bonne pratique consiste à pousser la frontière le plus bas possible, vers les feuilles de l'arbre : un bouton « Ajouter au panier », un menu déroulant, un formulaire.
// app/products/[id]/page.tsx — Server Component (par défaut)
import { db } from "@/lib/db";
import { AddToCartButton } from "./add-to-cart-button";
export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const product = await db.product.findUnique({ where: { id } });
return (
<article>
<h1>{product.name}</h1>
<p>{product.description}</p>
{/* Seul ce bouton est envoyé au navigateur */}
<AddToCartButton productId={product.id} />
</article>
);
}// app/products/[id]/add-to-cart-button.tsx
"use client";
import { useState } from "react";
export function AddToCartButton({ productId }: { productId: string }) {
const [added, setAdded] = useState(false);
return (
<button onClick={() => setAdded(true)}>
{added ? "Ajouté ✓" : "Ajouter au panier"}
</button>
);
}Les règles à retenir
- Un Server Component peut importer un Client Component, l'inverse n'est pas possible directement.
- Un Client Component peut recevoir un Server Component via `children` (ou n'importe quelle prop de type ReactNode). C'est le pattern pour garder un layout interactif (un tiroir, des onglets) autour de contenu rendu côté serveur.
- Les props qui traversent la frontière doivent être sérialisables : chaînes, nombres, objets simples, tableaux, dates… mais pas de fonctions ni d'instances de classes.
- Le chargement de données se fait directement dans le composant, avec
await, au plus près de là où la donnée est utilisée. Next.js déduplique les requêtes identiques pendant un même rendu.
Les pièges fréquents
- Importer un module serveur dans un Client Component (client de base de données, variables d'environnement secrètes). Le package
server-onlypermet de faire échouer le build si cela arrive. - Utiliser un Context React pour tout : un Provider est forcément un Client Component. Il faut l'isoler dans un petit fichier et l'utiliser comme wrapper avec
children. - Créer des cascades côté serveur : deux
awaitsuccessifs indépendants s'exécutent l'un après l'autre. UtilisezPromise.allquand les requêtes ne dépendent pas l'une de l'autre.
Conclusion
Les Server Components ne sont pas une simple optimisation : ils changent la répartition du travail entre serveur et navigateur. En gardant l'interactivité dans de petites « îles » client et tout le reste côté serveur, on obtient des bundles plus légers, un chargement de données plus simple et des pages plus rapides. La règle d'or : serveur par défaut, client seulement quand c'est nécessaire.