Server Component et Client Component dans Next.js
1. Aperçu
Next.js App Router rend par défaut les composants côté serveur, ce qui diffère complètement du modèle « tout côté client » du Pages Router d’avant. Cet article vous aide à comprendre les différences entre Server Component et Client Component, quand vous devez absolument utiliser "use client", et comment combiner ces deux types de composants dans un même arbre de rendu sans erreurs. Après l’avoir lu, vous serez en mesure de décider si un composant doit résider côté serveur ou client, et vous saurez comment transmettre les données à travers cette limite de manière correcte.
2. Qu’est-ce qu’un Server Component
Server Component est le type de composant par défaut dans App Router (dossier app/). Il s’exécute entièrement côté serveur et n’envoie pas son propre JavaScript au navigateur.
// app/products/page.tsx
async function getProducts() {
const res = await fetch("https://api.shop.local/products", { cache: "no-store" });
return res.json();
}
export default async function ProductsPage() {
const products = await getProducts();
return (
<ul>
{products.map((p: { id: number; name: string }) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}
Quelques points à noter :
ProductsPageest uneasync function— les Server Component peuventawaitdirectement, sans besoin deuseEffectni deuseStatepour récupérer les données.- L’appel
fetchs’exécute côté serveur, les données API, les clés secrètes ou la logique sensible ne sont jamais exposées au navigateur. - Il n’y a pas de ligne
"use client"en haut du fichier, donc Next.js comprend automatiquement qu’il s’agit d’un Server Component.
2.1. Pourquoi privilégier les Server Component
Les Server Component réduisent la taille du bundle JavaScript envoyé au client, car le composant lui-même (et ses importations, s’il ne sont pas convertis en client) n’est pas empaqueté dans le bundle du navigateur. Ils accèdent également directement à la base de données, au système de fichiers ou aux variables d’environnement secrètes sans avoir besoin de les exposer via une route API.
Requête → Rendu côté serveur (fetch, requête BD) → HTML + RSC payload → Navigateur
Le navigateur reçoit uniquement le HTML pré-rendu ainsi qu’une charge utile décrivant l’arbre des composants (RSC payload), il ne reçoit pas le code source JS du Server Component.
3. Qu’est-ce qu’un Client Component
Client Component est un composant qui doit s’exécuter dans le navigateur — pour utiliser state, effect, gestionnaires d’événements ou des API Web comme localStorage, window. Vous le déclarez avec la directive "use client" au début du fichier.
// app/components/LikeButton.tsx
"use client";
import { useState } from "react";
export default function LikeButton() {
const [liked, setLiked] = useState(false);
return (
<button onClick={() => setLiked(!liked)}>
{liked ? "❤️ Aimé" : "🤍 J'aime"}
</button>
);
}
Explication :
"use client"doit être la première ligne du fichier (avant les imports), elle marque une limite — tout ce que vous importez de ce fichier est également empaqueté pour le client.useStatene fonctionne que dans les Client Component, car il a besoin de JavaScript s’exécutant dans le navigateur pour gérer le re-rendu.onClickest un gestionnaire d’événement — c’est un signe certain que vous avez besoin d’un Client Component.
Note :
"use client"ne signifie pas que le composant s’exécute uniquement côté client. Next.js le rend toujours d’abord côté serveur (SSR) pour avoir du HTML rapide, puis le « réhydrate » dans le navigateur pour le rendre interactif.
4. Quand vous devez absolument utiliser Client Component
Il y a plusieurs cas où vous devez ajouter "use client", sinon vous rencontrerez des erreurs de build ou runtime :
Error: useState can not be used in Server Components.
Ces cas incluent :
- Utiliser des hooks React :
useState,useEffect,useReducer,useContext… - Ajouter des gestionnaires d’événements :
onClick,onChange,onSubmit… - Utiliser des API Web uniquement disponibles dans le navigateur :
window,document,localStorage. - Utiliser un hook personnalisé qui contient l’un des éléments ci-dessus.
- Utiliser une bibliothèque côté client uniquement (par exemple certaines bibliothèques de graphiques basées sur
window).
À l’inverse, si un composant reçoit simplement des props et rend du JSX pur, sans hooks ni événements — laissez-le comme Server Component, même s’il se trouve profondément dans un arbre avec d’autres Client Component.
5. Combiner Server Component et Client Component
Le point fort de ce modèle est que vous pouvez imbriquer des Server Component dans des Client Component via children (composition), plutôt que de les importer directement.
// app/components/Modal.tsx
"use client";
import { useState } from "react";
export default function Modal({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>Afficher les détails</button>
{open && <div className="modal">{children}</div>}
</>
);
}
// app/orders/[id]/page.tsx
import Modal from "@/app/components/Modal";
async function getOrder(id: string) {
const res = await fetch(`https://api.shop.local/orders/${id}`);
return res.json();
}
export default async function OrderPage({ params }: { params: { id: string } }) {
const order = await getOrder(params.id);
return (
<Modal>
{/* OrderDetail reste un Server Component, il est simplement « passé » comme children */}
<p>Numéro de commande : {order.id}</p>
<p>Total : {order.total.toLocaleString("fr-FR")} €</p>
</Modal>
);
}
Explication du mécanisme :
Modalest un Client Component qui gère l’état ouvert/fermé.- Le contenu à l’intérieur (
<p>Numéro de commande...) est rendu dansOrderPage— un Server Component — puis transmis àModalen tant quechildren. - Next.js rend cette partie
childrencôté serveur d’abord, puis « l’insère » à la place de{children}du Client Component lors de la réhydratation. Grâce à cela,OrderService.getOrdern’a pas besoin d’être convertie côté client.
Note : Vous ne pouvez pas importer directement un Server Component
asyncpuis l’utiliser à l’intérieur d’un Client Component (par exempleimport OrderDetail from "./OrderDetail"dans un fichier avec"use client"). Next.js considérera queOrderDetailest tiré dans le bundle client et signalera une erreur car uneasync function componentn’est pas valide côté client. La composition viachildrenoupropsest la bonne approche.
6. Transmettre des données d’un Server Component à un Client Component
Les props transmises d’un Server Component vers un Client Component doivent être sérialisables — c’est-à-dire convertibles en JSON. Vous ne pouvez pas transmettre une fonction, une instance de classe ou un objet Date complexe.
// app/components/PriceTag.tsx
"use client";
export default function PriceTag({ amount, currency }: { amount: number; currency: string }) {
return <span>{amount.toLocaleString("fr-FR")} {currency}</span>;
}
// app/products/[id]/page.tsx
import PriceTag from "@/app/components/PriceTag";
export default async function ProductDetailPage({ params }: { params: { id: string } }) {
const product = await fetch(`https://api.shop.local/products/${params.id}`).then(r => r.json());
return (
<div>
<h1>{product.name}</h1>
<PriceTag amount={product.price} currency="EUR" />
</div>
);
}
Ici amount (number) et currency (string) sont des données primitives sérialisables. Si vous essayez de transmettre une fonction callback d’un Server Component à PriceTag, Next.js signalera une erreur au build/runtime :
Error: Functions cannot be passed directly to Client Components
unless you explicitly expose it by marking it with "use server".
Pour appeler une logique côté serveur depuis le client (par exemple soumettre un formulaire), utilisez une Server Action — marquée avec "use server" — au lieu de transmettre une fonction ordinaire.
7. Server Action — le pont retour
Quand un Client Component a besoin d’appeler le serveur (écrire des données, interroger la BD), utilisez une Server Action au lieu de créer une route API séparée.
// app/actions/order-actions.ts
"use server";
export async function cancelOrder(orderId: string) {
await fetch(`https://api.shop.local/orders/${orderId}/cancel`, { method: "POST" });
}
// app/components/CancelButton.tsx
"use client";
import { cancelOrder } from "@/app/actions/order-actions";
import { useState } from "react";
export default function CancelButton({ orderId }: { orderId: string }) {
const [pending, setPending] = useState(false);
async function handleClick() {
setPending(true);
await cancelOrder(orderId);
setPending(false);
}
return (
<button onClick={handleClick} disabled={pending}>
{pending ? "Annulation en cours..." : "Annuler la commande"}
</button>
);
}
cancelOrderest marquée"use server", Next.js génère automatiquement un point de terminaison caché pour que le Client Component puisse appeler le serveur sans que vous ayez besoin d’écrire un gestionnaire de route.CancelButtonl’importe et l’appelle comme une fonction ordinaire — le code reste naturel à lire, même s’il s’agit en réalité d’un appel réseau.
8. Écrire des tests pour la logique séparée
Puisque les Server Component contiennent généralement de la logique fetch/transformation de données, séparez la logique pure (sans JSX) pour la tester plus facilement que de tester l’ensemble du rendu du composant.
// app/lib/pricing.ts
export function formatPrice(amount: number, currency: string): string {
if (amount < 0) {
throw new Error("le montant ne peut pas être négatif");
}
return `${amount.toLocaleString("fr-FR")} ${currency}`;
}
// app/lib/pricing.test.ts
import { describe, it, expect } from "vitest";
import { formatPrice } from "./pricing";
describe("formatPrice", () => {
it("formate un montant valide", () => {
expect(formatPrice(150000, "EUR")).toBe("150.000,00 EUR");
});
it("lève une erreur quand le montant est négatif", () => {
expect(() => formatPrice(-1, "EUR")).toThrow("le montant ne peut pas être négatif");
});
});
Si votre projet utilise Jest + Testing Library pour les Client Component (par exemple LikeButton), testez-le comme un composant React ordinaire — Jest n’a pas besoin de savoir quoi que ce soit sur "use client", car cette directive n’a un sens que pour le bundler de Next.js, elle n’affecte pas l’exécution des tests.
// app/components/LikeButton.test.tsx
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import LikeButton from "./LikeButton";
describe("LikeButton", () => {
it("change d'état au clic", () => {
render(<LikeButton />);
const button = screen.getByRole("button");
expect(button).toHaveTextContent("J'aime");
fireEvent.click(button);
expect(button).toHaveTextContent("Aimé");
});
});
9. Erreurs courantes
Quelques erreurs fréquentes quand on passe au App Router :
- Oublier
"use client"quand utiliser un hook : erreuruseState can not be used in Server Components— ajoutez la directive au début du fichier qui contient le hook, pas du fichier qui l’appelle. - Placer
"use client"trop haut dans l’arbre des composants : si vous marquez tout unlayout.tsxcomme client, tout ce qui est à l’intérieur (y compris les Server Component enfants) sera tiré dans le bundle client s’il est importé directement, ce qui gonfle inutilement le bundle. Convertissez en client uniquement les plus petits composants nécessaires (par exemple un bouton, une entrée de formulaire), pas la page entière. - Transmettre une fonction ou un objet non sérialisable via les props d’un Server Component vers un Client Component — utilisez une Server Action (
"use server") au lieu de transmettre une fonction callback. - Importer un Server Component à l’intérieur d’un fichier
"use client"puis espérer qu’il s’exécute toujours côté serveur — ce n’est pas possible, vous devez utiliser la composition viachildren/props. - Appeler
fetchavec une clé secrète dans un Client Component — la clé API sera exposée dans le bundle JS envoyé au navigateur. Les opérations nécessitant un secret doivent toujours résider dans un Server Component ou une Server Action.
Note : Convertir tous les composants en Client Component « juste pour revenir au ancien code » n’est pas une bonne idée. Chaque Client Component inutile augmente la taille du bundle, ralentit le temps jusqu’à l’interactivité (TTI) et perd les bénéfices du rendu côté serveur.
10. Conclusion
Server Component est par défaut dans App Router, idéal pour récupérer les données, accéder aux ressources sensibles et réduire le bundle JS. Client Component ne doit être utilisé que s’il y a state, effect ou gestionnaire d’événement. Ces deux types se combinent via la composition (children, props sérialisables) et les Server Action qui servent de pont pour que les Client Component rappellent de la logique serveur. Bien comprendre cette limite vous aide à optimiser la taille du bundle et à garder les données sensibles en sécurité côté serveur.