Depuis le passage à l'App Router dans Next.js 13+, une question revient systématiquement dans les discussions entre développeurs : quand utiliser un Server Component, et quand utiliser un Client Component ? Si vous avez déjà eu une erreur useState is not defined ou un mystérieux bug d'hydratation, cet article est fait pour vous.
On va démystifier ce modèle ensemble, comprendre les règles du jeu, et surtout éviter les pièges classiques.
Pourquoi cette distinction existe-t-elle ?
Avant l'App Router, React rendait tout côté client par défaut (avec le SSR comme couche optionnelle). Chaque composant embarquait son JavaScript dans le bundle envoyé au navigateur, même s'il ne faisait qu'afficher du texte statique.
Les React Server Components (RSC) renversent ce paradigme : par défaut, dans le dossier app/, tous les composants sont des Server Components. Ils s'exécutent exclusivement sur le serveur, ne génèrent aucun JavaScript côté client, et peuvent accéder directement aux bases de données, au système de fichiers ou aux API privées.
Les Client Components, eux, s'exécutent dans le navigateur (avec un pré-rendu serveur optionnel). Ils sont nécessaires dès qu'on a besoin d'interactivité ou d'état local.
Le résultat concret : des pages plus légères, des performances améliorées, et un meilleur contrôle sur ce qui s'exécute où.
Les règles fondamentales à retenir
Les Server Components : ce qu'ils peuvent (et ne peuvent pas) faire
Un Server Component peut :
- Faire des appels
fetchavec accès aux credentials serveur - Lire directement dans une base de données (Mongoose, Prisma, etc.)
- Accéder aux variables d'environnement serveur (
process.env.SECRET_KEY) - Importer des modules Node.js (
fs,path, etc.) - Rendre des Client Components en leur passant des props
Un Server Component ne peut pas :
- Utiliser
useState,useEffect,useReduceret autres hooks React - Accéder aux APIs navigateur (
window,document,localStorage) - Gérer des event listeners (
onClick,onChange…) - Utiliser du contexte React classique (sauf via des patterns spécifiques)
Les Client Components : le marqueur "use client"
Pour transformer un composant en Client Component, il suffit d'ajouter la directive "use client" en première ligne du fichier :
"use client";
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Clics : {count}
</button>
);
}
Important : "use client" définit une frontière. Tous les composants importés dans ce fichier deviennent automatiquement des Client Components, même s'ils n'ont pas la directive eux-mêmes. C'est un point souvent mal compris.
L'arbre de composition : comment ça s'organise concrètement ?
Voici le pattern le plus courant en pratique :
app/
page.tsx ← Server Component (accès BDD, fetch)
└── ProductList ← Server Component (logique métier)
└── AddToCartButton ← Client Component (interactivité)
Un Server Component peut rendre un Client Component, mais un Client Component ne peut pas rendre un Server Component directement (enfin, pas vraiment — on y revient).
// app/page.tsx (Server Component)
import { getProducts } from "@/lib/db";
import { ProductList } from "@/components/ProductList";
export default async function HomePage() {
const products = await getProducts(); // accès BDD direct
return <ProductList products={products} />;
}
// components/ProductList.tsx (Server Component)
import { AddToCartButton } from "./AddToCartButton";
export function ProductList({ products }: { products: Product[] }) {
return (
<ul>
{products.map((product) => (
<li key={product.id}>
<span>{product.name}</span>
<AddToCartButton productId={product.id} />
</li>
))}
</ul>
);
}
// components/AddToCartButton.tsx (Client Component)
"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>
);
}
Cette architecture est optimale : toute la logique de récupération de données reste serveur, seule l'interactivité part dans le bundle client.
Le pattern "children as slot" : passer des Server Components dans des Client Components
Voici une astuce puissante et peu connue. Même si un Client Component ne peut pas importer un Server Component, il peut en recevoir un via children (ou une prop quelconque). Ce pattern permet de garder des parties de l'arbre en mode serveur :
// components/Modal.tsx (Client Component)
"use client";
import { useState, ReactNode } from "react";
export function Modal({ children }: { children: ReactNode }) {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>Ouvrir</button>
{open && <div className="modal">{children}</div>}
</>
);
}
// app/page.tsx (Server Component)
import { Modal } from "@/components/Modal";
import { HeavyServerContent } from "@/components/HeavyServerContent";
export default function Page() {
return (
<Modal>
<HeavyServerContent /> {/* Reste un Server Component */}
</Modal>
);
}
HeavyServerContent sera rendu côté serveur, même s'il est "à l'intérieur" d'un Client Component. Très utile pour garder des sections data-intensive hors du bundle client.
Les erreurs classiques (et comment les éviter)
❌ Erreur 1 : Mettre "use client" trop haut dans l'arbre
C'est le piège numéro un. Si vous ajoutez "use client" à un composant parent très haut dans la hiérarchie (un layout, par exemple), vous faites basculer tous ses enfants en mode client, perdant tous les bénéfices des RSC.
Règle d'or : poussez "use client" aussi bas que possible dans l'arbre. Seul le composant qui a réellement besoin d'interactivité doit l'avoir.
❌ Erreur 2 : Sérialiser des données non-sérialisables
Quand vous passez des props d'un Server Component vers un Client Component, ces données doivent être sérialisables (JSON-compatible). Des instances de classe, des fonctions, des Date non converties… tout ça va planter.
// ❌ À éviter
<ClientComponent user={userMongooseDocument} />
// ✅ Sérialiser avant de passer
<ClientComponent user={{ id: user.id, name: user.name, email: user.email }} />
❌ Erreur 3 : Fetch côté client quand le serveur suffit
Beaucoup de développeurs gardent le réflexe useEffect + fetch pour charger des données. Dans l'App Router, si vos données ne changent pas dynamiquement côté client, faites-le côté serveur :
// ❌ Pattern à abandonner (quand possible)
"use client";
useEffect(() => {
fetch("/api/products").then(...);
}, []);
// ✅ Préférer le Server Component async
export default async function Page() {
const products = await fetch("https://api.example.com/products").then(r => r.json());
return <ProductList products={products} />;
}
Un mot sur les performances
La différence de performance est concrète. Un Server Component ne génère aucun JavaScript dans le bundle client. Pour une application avec beaucoup de contenu affiché (fiches produit, articles de blog, dashboards de lecture), le gain peut être significatif : moins de code à parser, moins de mémoire consommée, meilleur Time to Interactive.
Next.js va même plus loin avec le streaming et Suspense : les Server Components lents (fetch BDD) peuvent streamer leur contenu progressivement sans bloquer le reste de la page. C'est une des raisons pour lesquelles l'App Router est une avancée majeure.
En résumé : le guide de décision rapide
| Je dois… | → Utiliser |
|---|---|
| Récupérer des données en BDD | Server Component (async) |
| Afficher du contenu statique | Server Component |
Gérer du state (useState) | Client Component |
Réagir à des événements (onClick) | Client Component |
Accéder à localStorage / window | Client Component |
Utiliser useEffect | Client Component |
| Mélanger les deux | Pattern children as slot |
La transition vers ce modèle demande un peu de reconfiguration mentale, mais une fois qu'on l'a intégré, l'architecture devient beaucoup plus claire et les performances s'en ressentent immédiatement.
Si vous travaillez sur une application Next.js et que vous souhaitez un regard extérieur sur votre architecture ou un accompagnement dans votre projet, n'hésitez pas à me contacter — c'est exactement le genre de problématiques sur lesquelles j'accompagne mes clients au quotidien.